Compare commits
12 Commits
main
..
66bb4c4c7c
| Author | SHA1 | Date | |
|---|---|---|---|
| 66bb4c4c7c | |||
| f167f233b7 | |||
| e7d437f201 | |||
| 14120dc683 | |||
| 9135ccd4dd | |||
| 7d015958ba | |||
| 0850faef36 | |||
| f0dd4a4c56 | |||
| 9892293a7e | |||
| 346d67a15b | |||
| 330a0534d9 | |||
| cd376094df |
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
<?php
|
||||
/**
|
||||
* jalan.php
|
||||
* Tanggung Jawab: Menangani operasi CRUD untuk entitas Jalan (LineString).
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
||||
require_once '../core_config/database.php';
|
||||
require_once '../utils/response_helper.php';
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
<?php
|
||||
/**
|
||||
* kavling.php
|
||||
* Tanggung Jawab: Menangani operasi CRUD untuk entitas Kavling (Polygon).
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
||||
require_once '../core_config/database.php';
|
||||
require_once '../utils/response_helper.php';
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
@@ -21,7 +21,6 @@ switch ($method) {
|
||||
'geometry' => json_decode($row['geojson']),
|
||||
'properties' => [
|
||||
'id' => $row['id'],
|
||||
'nama' => $row['nama_pemilik'],
|
||||
'nama_pemilik' => $row['nama_pemilik'],
|
||||
'luas' => $row['luas'],
|
||||
'created_at' => $row['created_at']
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
||||
<?php
|
||||
require_once '../core_config/database.php';
|
||||
require_once '../utils/response_helper.php';
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
|
||||
+87
-265
@@ -1,88 +1,33 @@
|
||||
/* ═══════════════════════════════════════════════════
|
||||
WebGIS P01 — main.css | Dark / Light Theme
|
||||
═══════════════════════════════════════════════════ */
|
||||
|
||||
/* ── Design Tokens ── */
|
||||
:root {
|
||||
--ease: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition: all 0.25s var(--ease);
|
||||
--primary: #6366F1;
|
||||
--primary-hover: #4F46E5;
|
||||
--bg-dark: #0F172A;
|
||||
--bg-glass: rgba(15, 23, 42, 0.75);
|
||||
--border-glass: rgba(255, 255, 255, 0.1);
|
||||
--text-light: #F8FAFC;
|
||||
--text-muted: #94A3B8;
|
||||
--radius: 20px;
|
||||
--shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
--font: 'Inter', sans-serif;
|
||||
--radius: 16px;
|
||||
}
|
||||
|
||||
/* ── Dark Theme (default) ── */
|
||||
[data-theme="dark"] {
|
||||
--primary: #818CF8;
|
||||
--primary-deep: #6366F1;
|
||||
--primary-glow: rgba(99,102,241,0.3);
|
||||
--bg-dark: #0A0F1E;
|
||||
--bg-glass: rgba(17,24,39,0.82);
|
||||
--bg-input: rgba(255,255,255,0.05);
|
||||
--border-glass: rgba(255,255,255,0.08);
|
||||
--border-focus: #6366F1;
|
||||
--text-main: #F1F5F9;
|
||||
--text-muted: #94A3B8;
|
||||
--shadow: 0 25px 50px -12px rgba(0,0,0,0.55);
|
||||
--dot-color: rgba(255,255,255,0.025);
|
||||
--btn-spbu: #10B981;
|
||||
--btn-jalan: #F59E0B;
|
||||
--btn-kavling: #3B82F6;
|
||||
--btn-rumah: #8B5CF6;
|
||||
--btn-warga: #EF4444;
|
||||
}
|
||||
|
||||
/* ── Light Theme ── */
|
||||
[data-theme="light"] {
|
||||
--primary: #4F46E5;
|
||||
--primary-deep: #3730A3;
|
||||
--primary-glow: rgba(79,70,229,0.15);
|
||||
--bg-dark: #F0F4FF;
|
||||
--bg-glass: rgba(255,255,255,0.92);
|
||||
--bg-input: #FFFFFF;
|
||||
--border-glass: #E2E8F0;
|
||||
--border-focus: #4F46E5;
|
||||
--text-main: #0F172A;
|
||||
--text-muted: #64748B;
|
||||
--shadow: 0 25px 50px -12px rgba(79,70,229,0.12);
|
||||
--dot-color: rgba(79,70,229,0.035);
|
||||
--btn-spbu: #059669;
|
||||
--btn-jalan: #D97706;
|
||||
--btn-kavling: #2563EB;
|
||||
--btn-rumah: #7C3AED;
|
||||
--btn-warga: #DC2626;
|
||||
}
|
||||
|
||||
/* ── Reset ── */
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--font);
|
||||
background: var(--bg-dark);
|
||||
color: var(--text-main);
|
||||
color: var(--text-light);
|
||||
overflow: hidden;
|
||||
transition: background 0.3s var(--ease), color 0.3s var(--ease);
|
||||
}
|
||||
|
||||
/* Dot grid background */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-image: radial-gradient(circle, var(--dot-color) 1px, transparent 1px);
|
||||
background-size: 28px 28px;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* ── Map ── */
|
||||
#map {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* ── Floating Dock ── */
|
||||
/* Floating Dock */
|
||||
.floating-dock {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
@@ -92,178 +37,90 @@ body::before {
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
border: 1px solid var(--border-glass);
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
width: 320px;
|
||||
border-radius: var(--radius);
|
||||
padding: 24px;
|
||||
width: 340px;
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: 20px;
|
||||
max-height: calc(100vh - 48px);
|
||||
overflow-y: auto;
|
||||
transition: background 0.3s var(--ease), border-color 0.3s var(--ease);
|
||||
}
|
||||
|
||||
.floating-dock::-webkit-scrollbar { width: 4px; }
|
||||
.floating-dock::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 10px; }
|
||||
/* Custom Scrollbar for Dock */
|
||||
.floating-dock::-webkit-scrollbar { width: 6px; }
|
||||
.floating-dock::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 10px; }
|
||||
|
||||
/* ── Dock Top Bar ── */
|
||||
.dock-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
.header h2 { margin: 0 0 8px 0; font-size: 1.4rem; font-weight: 700; background: linear-gradient(135deg, #818CF8, #C084FC); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
||||
.header p { margin: 0; font-size: 0.85rem; color: var(--text-muted); line-height: 1.5; }
|
||||
|
||||
.btn-back-portal {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-glass);
|
||||
background: var(--bg-input);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: var(--transition);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn-back-portal:hover {
|
||||
background: var(--primary-deep);
|
||||
border-color: var(--primary-deep);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dock-controls {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.btn-theme-dock {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-glass);
|
||||
background: var(--bg-input);
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.8rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
.btn-theme-dock:hover {
|
||||
background: var(--primary-deep);
|
||||
border-color: var(--primary-deep);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* ── Header ── */
|
||||
.header h2 {
|
||||
margin: 0 0 6px 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, var(--primary), #C084FC);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
.header p {
|
||||
margin: 0;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── Buttons ── */
|
||||
.menu-group { display: flex; flex-direction: column; gap: 8px; }
|
||||
.menu-title {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-muted);
|
||||
font-weight: 700;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
/* Buttons */
|
||||
.menu-group { display: flex; flex-direction: column; gap: 10px; }
|
||||
.menu-title { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1px; color: var(--text-muted); font-weight: 600; margin-bottom: 4px; }
|
||||
|
||||
.btn-action {
|
||||
background: var(--bg-input);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid var(--border-glass);
|
||||
color: var(--text-main);
|
||||
padding: 11px 14px;
|
||||
color: var(--text-light);
|
||||
padding: 14px 16px;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
font-family: var(--font);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
transition: var(--transition);
|
||||
font-size: 0.875rem;
|
||||
gap: 12px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-size: 0.9rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.btn-action svg { width: 18px; height: 18px; flex-shrink: 0; }
|
||||
.btn-action svg { width: 20px; height: 20px; flex-shrink: 0; }
|
||||
|
||||
.btn-action:hover {
|
||||
background: rgba(255,255,255,0.08);
|
||||
transform: translateX(3px);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
[data-theme="light"] .btn-action:hover {
|
||||
background: #EEF2FF;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.btn-action.active {
|
||||
background: var(--primary-deep);
|
||||
border-color: var(--primary-deep);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px var(--primary-glow);
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 10px 15px -3px rgba(99, 102, 241, 0.4);
|
||||
}
|
||||
|
||||
/* Button color variants */
|
||||
.btn-spbu svg { color: var(--btn-spbu); }
|
||||
.btn-jalan svg { color: var(--btn-jalan); }
|
||||
.btn-kavling svg { color: var(--btn-kavling); }
|
||||
.btn-rumah svg { color: var(--btn-rumah); }
|
||||
.btn-warga svg { color: var(--btn-warga); }
|
||||
/* Button Variants */
|
||||
.btn-spbu svg { color: #10B981; }
|
||||
.btn-jalan svg { color: #F59E0B; }
|
||||
.btn-kavling svg { color: #3B82F6; }
|
||||
.btn-rumah svg { color: #8B5CF6; }
|
||||
.btn-warga svg { color: #EF4444; }
|
||||
|
||||
/* ── Form Panel ── */
|
||||
/* Form Panel */
|
||||
.form-panel {
|
||||
background: rgba(0,0,0,0.3);
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--border-glass);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
[data-theme="light"] .form-panel {
|
||||
background: rgba(79,70,229,0.04);
|
||||
}
|
||||
|
||||
.form-group { display: flex; flex-direction: column; gap: 5px; }
|
||||
.form-group label { font-size: 0.78rem; color: var(--text-muted); font-weight: 500; }
|
||||
|
||||
.form-group { display: flex; flex-direction: column; gap: 6px; }
|
||||
.form-group label { font-size: 0.8rem; color: var(--text-muted); }
|
||||
.form-control {
|
||||
background: var(--bg-input);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid var(--border-glass);
|
||||
padding: 9px 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
color: var(--text-main);
|
||||
color: white;
|
||||
font-family: var(--font);
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.9rem;
|
||||
outline: none;
|
||||
transition: border 0.2s;
|
||||
}
|
||||
.form-control:focus { border-color: var(--border-focus); box-shadow: 0 0 0 3px var(--primary-glow); }
|
||||
.form-control::placeholder { color: var(--text-muted); }
|
||||
.form-control:focus { border-color: var(--primary); }
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
@@ -271,113 +128,78 @@ body::before {
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.checkbox-group input { width: 15px; height: 15px; accent-color: var(--primary); cursor: pointer; }
|
||||
.checkbox-group span { font-size: 0.82rem; color: var(--text-main); }
|
||||
.checkbox-group input { width: 16px; height: 16px; accent-color: var(--primary); cursor: pointer; }
|
||||
.checkbox-group span { font-size: 0.85rem; color: var(--text-light); }
|
||||
|
||||
.btn-submit {
|
||||
background: var(--primary-deep);
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 11px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-family: var(--font);
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
font-size: 0.875rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.btn-submit:hover { filter: brightness(1.1); transform: translateY(-1px); }
|
||||
.btn-submit:hover { background: var(--primary-hover); }
|
||||
|
||||
/* ── Leaflet Customizations ── */
|
||||
/* Leaflet Customizations */
|
||||
.leaflet-draw-toolbar { display: none !important; }
|
||||
.leaflet-control-zoom { border: none !important; box-shadow: var(--shadow) !important; }
|
||||
.leaflet-control-zoom a {
|
||||
background: var(--bg-glass) !important;
|
||||
color: var(--text-main) !important;
|
||||
color: var(--text-light) !important;
|
||||
border-color: var(--border-glass) !important;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.leaflet-control-zoom a:hover { background: var(--primary-deep) !important; color: white !important; }
|
||||
|
||||
/* Custom Leaflet Layer Control style to match theme */
|
||||
.leaflet-control-layers {
|
||||
background: var(--bg-glass) !important;
|
||||
backdrop-filter: blur(24px) !important;
|
||||
-webkit-backdrop-filter: blur(24px) !important;
|
||||
border: 1px solid var(--border-glass) !important;
|
||||
border-radius: 14px !important;
|
||||
box-shadow: var(--shadow) !important;
|
||||
color: var(--text-main) !important;
|
||||
font-family: var(--font) !important;
|
||||
padding: 10px 14px !important;
|
||||
transition: var(--transition);
|
||||
}
|
||||
.leaflet-control-layers-expanded {
|
||||
min-width: 140px;
|
||||
}
|
||||
.leaflet-control-layers label {
|
||||
margin-bottom: 6px !important;
|
||||
font-size: 0.85rem !important;
|
||||
cursor: pointer !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 8px !important;
|
||||
color: var(--text-main) !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
.leaflet-control-layers label:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.leaflet-control-layers input[type="checkbox"] {
|
||||
cursor: pointer !important;
|
||||
width: 15px !important;
|
||||
height: 15px !important;
|
||||
accent-color: var(--primary-deep) !important;
|
||||
}
|
||||
.leaflet-control-zoom a:hover { background: var(--primary) !important; }
|
||||
|
||||
/* Leaflet Popup Premium */
|
||||
.leaflet-popup-content-wrapper {
|
||||
background: var(--bg-glass) !important;
|
||||
backdrop-filter: blur(16px) !important;
|
||||
border: 1px solid var(--border-glass) !important;
|
||||
color: var(--text-main) !important;
|
||||
border-radius: 14px !important;
|
||||
color: var(--text-light) !important;
|
||||
border-radius: 16px !important;
|
||||
box-shadow: var(--shadow) !important;
|
||||
}
|
||||
.leaflet-popup-tip { background: var(--bg-glass) !important; }
|
||||
.leaflet-popup-content { margin: 14px !important; font-family: var(--font); }
|
||||
.leaflet-popup-content h3 { margin: 0 0 8px 0; font-size: 1rem; border-bottom: 1px solid var(--border-glass); padding-bottom: 8px; }
|
||||
.leaflet-popup-content p { margin: 4px 0; font-size: 0.85rem; color: var(--text-muted); }
|
||||
|
||||
.leaflet-popup-tip { background: var(--bg-glass) !important; border: 1px solid var(--border-glass) !important; }
|
||||
.leaflet-popup-content { margin: 16px !important; font-family: var(--font); }
|
||||
.leaflet-popup-content h3 { margin: 0 0 8px 0; font-size: 1.1rem; border-bottom: 1px solid var(--border-glass); padding-bottom: 8px; }
|
||||
.leaflet-popup-content p { margin: 4px 0; font-size: 0.9rem; color: var(--text-muted); }
|
||||
.leaflet-popup-content .btn-delete {
|
||||
background: rgba(239,68,68,0.1);
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #EF4444;
|
||||
border: 1px solid rgba(239,68,68,0.3);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
margin-top: 10px;
|
||||
margin-top: 12px;
|
||||
width: 100%;
|
||||
font-family: var(--font);
|
||||
font-size: 0.85rem;
|
||||
transition: var(--transition);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.leaflet-popup-content .btn-delete:hover { background: #EF4444; color: white; }
|
||||
|
||||
/* ── Toast ── */
|
||||
/* Custom Map Marker Icon Animation */
|
||||
.custom-icon div {
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.custom-icon:hover div {
|
||||
transform: scale(1.1) translateY(-4px);
|
||||
}
|
||||
|
||||
/* Toast Notification */
|
||||
.toast-container {
|
||||
position: fixed; bottom: 24px; right: 24px; z-index: 9999;
|
||||
display: flex; flex-direction: column; gap: 10px;
|
||||
}
|
||||
.toast {
|
||||
background: var(--bg-glass); backdrop-filter: blur(16px);
|
||||
border: 1px solid var(--border-glass); border-left: 4px solid var(--primary-deep);
|
||||
padding: 14px 18px; border-radius: 12px; color: var(--text-main); font-size: 0.875rem;
|
||||
box-shadow: var(--shadow); animation: slideIn 0.3s var(--ease);
|
||||
border: 1px solid var(--border-glass); border-left: 4px solid var(--primary);
|
||||
padding: 16px 20px; border-radius: 12px; color: white; font-size: 0.9rem;
|
||||
box-shadow: var(--shadow); animation: slideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.toast.error { border-left-color: #EF4444; }
|
||||
.toast.error { border-left-color: #EF4444; }
|
||||
.toast.success { border-left-color: #10B981; }
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
||||
|
||||
+8
-94
@@ -1,95 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id" data-theme="dark">
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebGIS — Pertemuan 01: Geometri Dasar</title>
|
||||
<meta name="description" content="Modul 01 — Menggambar dan mengelola geometri spasial (titik, garis, poligon) berbasis WebGIS.">
|
||||
<title>WebGIS - Pertemuan 01</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="css/main.css">
|
||||
<script>
|
||||
// Global error interceptor to assist in visual debugging
|
||||
window.onerror = function(message, source, lineno, colno, error) {
|
||||
const errorMsg = `JS Error: ${message} (at ${source ? source.split('/').pop() : 'unknown'}:${lineno}:${colno})`;
|
||||
console.error(errorMsg, error);
|
||||
const showInterval = setInterval(() => {
|
||||
const toastContainer = document.getElementById('toast-container');
|
||||
if (toastContainer) {
|
||||
clearInterval(showInterval);
|
||||
const t = document.createElement('div');
|
||||
t.className = 'toast error';
|
||||
t.innerHTML = errorMsg;
|
||||
toastContainer.appendChild(t);
|
||||
}
|
||||
}, 100);
|
||||
return false;
|
||||
};
|
||||
window.onunhandledrejection = function(event) {
|
||||
const errorMsg = `Promise Error: ${event.reason?.message || event.reason}`;
|
||||
console.error(errorMsg, event.reason);
|
||||
const showInterval = setInterval(() => {
|
||||
const toastContainer = document.getElementById('toast-container');
|
||||
if (toastContainer) {
|
||||
clearInterval(showInterval);
|
||||
const t = document.createElement('div');
|
||||
t.className = 'toast error';
|
||||
t.innerHTML = errorMsg;
|
||||
toastContainer.appendChild(t);
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Map container -->
|
||||
<!-- Peta -->
|
||||
<div id="map"></div>
|
||||
|
||||
<!-- Floating Dock -->
|
||||
<div class="floating-dock">
|
||||
|
||||
<!-- Dock top bar: back + theme toggle -->
|
||||
<div class="dock-topbar">
|
||||
<a href="../../portal.html" class="btn-back-portal" title="Kembali ke Portal">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
Portal
|
||||
</a>
|
||||
<div class="dock-controls">
|
||||
<button class="btn-theme-dock" id="themeToggle" title="Toggle tema" aria-label="Toggle dark/light mode">
|
||||
<i class="fas fa-moon" id="themeIcon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<h2>P01: Geometri Dasar</h2>
|
||||
<p>Sistem Informasi Geografis — CRUD spasial titik, garis, dan area.</p>
|
||||
<h2>P01: Geometri</h2>
|
||||
<p>Sistem Informasi Geografis dengan arsitektur modern.</p>
|
||||
</div>
|
||||
|
||||
<!-- Layer Peta (Centang) -->
|
||||
<div class="menu-group">
|
||||
<div class="menu-title">Layer Peta</div>
|
||||
<div class="form-panel" style="gap: 10px;">
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" id="layer-spbu" checked>
|
||||
<span>Stasiun SPBU</span>
|
||||
</label>
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" id="layer-jalan" checked>
|
||||
<span>Jalur Jalan</span>
|
||||
</label>
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" id="layer-kavling" checked>
|
||||
<span>Kavling Tanah</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spatial tools -->
|
||||
<div class="menu-group">
|
||||
<div class="menu-title">Alat Spasial</div>
|
||||
|
||||
@@ -109,34 +40,17 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic form container -->
|
||||
<!-- Dynamic Form Container -->
|
||||
<div id="form-container" style="display: none;"></div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<!-- Toast Notification Container -->
|
||||
<div id="toast-container" class="toast-container"></div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
|
||||
<script>
|
||||
// ── Theme toggle ──
|
||||
const html = document.documentElement;
|
||||
const btn = document.getElementById('themeToggle');
|
||||
const icon = document.getElementById('themeIcon');
|
||||
const saved = localStorage.getItem('portal-theme') || 'dark';
|
||||
setTheme(saved);
|
||||
btn.addEventListener('click', () => {
|
||||
const next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
||||
setTheme(next);
|
||||
localStorage.setItem('portal-theme', next);
|
||||
});
|
||||
function setTheme(t) {
|
||||
html.setAttribute('data-theme', t);
|
||||
icon.className = t === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
|
||||
}
|
||||
</script>
|
||||
<script type="module" src="js/app.js?v=20260613-9"></script>
|
||||
<script type="module" src="js/app.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+21
-114
@@ -6,13 +6,6 @@ import { spbuService, jalanService, kavlingService } from './services/api.servic
|
||||
let appMap;
|
||||
let drawControl;
|
||||
|
||||
// Global layer object tracker
|
||||
const mapLayers = {
|
||||
spbu: null,
|
||||
jalan: null,
|
||||
kavling: null
|
||||
};
|
||||
|
||||
// Custom SVG Icons Generator
|
||||
export const createIcon = (svgPath, color) => {
|
||||
return L.divIcon({
|
||||
@@ -36,94 +29,28 @@ window.showToast = (msg, type='success') => {
|
||||
setTimeout(() => { t.style.transform='translateX(100%)'; t.style.opacity=0; setTimeout(()=>t.remove(),300); }, 3000);
|
||||
};
|
||||
|
||||
const setupCheckboxListeners = () => {
|
||||
const layerConfigs = [
|
||||
{ id: 'layer-spbu', key: 'spbu' },
|
||||
{ id: 'layer-jalan', key: 'jalan' },
|
||||
{ id: 'layer-kavling', key: 'kavling' }
|
||||
];
|
||||
|
||||
layerConfigs.forEach(({ id, key }) => {
|
||||
const checkbox = document.getElementById(id);
|
||||
if (checkbox) {
|
||||
checkbox.addEventListener('change', (e) => {
|
||||
const layer = mapLayers[key];
|
||||
if (layer) {
|
||||
if (e.target.checked) {
|
||||
if (!appMap.hasLayer(layer)) {
|
||||
appMap.addLayer(layer);
|
||||
}
|
||||
} else {
|
||||
if (appMap.hasLayer(layer)) {
|
||||
appMap.removeLayer(layer);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const loadSPBUData = async () => {
|
||||
try {
|
||||
const spbu = await spbuService.getAll();
|
||||
mapLayers.spbu = L.geoJSON(spbu, {
|
||||
pointToLayer: (f, latlng) => L.marker(latlng, { icon: iconSPBU(f.properties.buka_24_jam) }),
|
||||
onEachFeature: (f, l) => bindPopup(l, 'spbu', f.properties)
|
||||
});
|
||||
|
||||
const checkbox = document.getElementById('layer-spbu');
|
||||
if (checkbox && checkbox.checked) {
|
||||
mapLayers.spbu.addTo(appMap);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Gagal meload data SPBU:", e);
|
||||
window.showToast("Gagal meload data SPBU: " + e.message, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const loadJalanData = async () => {
|
||||
try {
|
||||
const jalan = await jalanService.getAll();
|
||||
mapLayers.jalan = L.geoJSON(jalan, {
|
||||
style: { color: '#F59E0B', weight: 4 },
|
||||
onEachFeature: (f, l) => bindPopup(l, 'jalan', f.properties)
|
||||
});
|
||||
|
||||
const checkbox = document.getElementById('layer-jalan');
|
||||
if (checkbox && checkbox.checked) {
|
||||
mapLayers.jalan.addTo(appMap);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Gagal meload data Jalan:", e);
|
||||
window.showToast("Gagal meload data Jalan: " + e.message, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const loadKavlingData = async () => {
|
||||
try {
|
||||
const kavling = await kavlingService.getAll();
|
||||
mapLayers.kavling = L.geoJSON(kavling, {
|
||||
style: { color: '#3B82F6', weight: 2, fillColor: '#3B82F6', fillOpacity: 0.3 },
|
||||
onEachFeature: (f, l) => bindPopup(l, 'kavling', f.properties)
|
||||
});
|
||||
|
||||
const checkbox = document.getElementById('layer-kavling');
|
||||
if (checkbox && checkbox.checked) {
|
||||
mapLayers.kavling.addTo(appMap);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Gagal meload data Kavling:", e);
|
||||
window.showToast("Gagal meload data Kavling: " + e.message, 'error');
|
||||
}
|
||||
};
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
appMap = initMap('map');
|
||||
drawControl = setupDrawControls(appMap, handleGeometryCreated);
|
||||
await loadAllData();
|
||||
});
|
||||
|
||||
const loadAllData = async () => {
|
||||
await Promise.allSettled([
|
||||
loadSPBUData(),
|
||||
loadJalanData(),
|
||||
loadKavlingData()
|
||||
]);
|
||||
try {
|
||||
const spbu = await spbuService.getAll();
|
||||
L.geoJSON(spbu, {
|
||||
pointToLayer: (f, latlng) => L.marker(latlng, { icon: iconSPBU(f.properties.buka_24_jam) }),
|
||||
onEachFeature: (f, l) => bindPopup(l, 'spbu', f.properties)
|
||||
}).addTo(appMap);
|
||||
|
||||
const jalan = await jalanService.getAll();
|
||||
L.geoJSON(jalan, { style: { color: '#F59E0B', weight: 4 }, onEachFeature: (f, l) => bindPopup(l, 'jalan', f.properties) }).addTo(appMap);
|
||||
|
||||
const kavling = await kavlingService.getAll();
|
||||
L.geoJSON(kavling, { style: { color: '#3B82F6', weight: 2, fillColor: '#3B82F6', fillOpacity: 0.3 }, onEachFeature: (f, l) => bindPopup(l, 'kavling', f.properties) }).addTo(appMap);
|
||||
} catch (e) {
|
||||
window.showToast("Gagal meload data: "+e.message, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const bindPopup = (layer, type, props) => {
|
||||
@@ -155,14 +82,7 @@ const handleGeometryCreated = (type, geometry, layer) => {
|
||||
try {
|
||||
if(type==='spbu') await spbuService.create(payload);
|
||||
if(type==='jalan') await jalanService.create(payload);
|
||||
if(type==='kavling') {
|
||||
const kavlingPayload = {
|
||||
nama_pemilik: payload.nama,
|
||||
luas: payload.luas || 0,
|
||||
geometry: payload.geometry
|
||||
};
|
||||
await kavlingService.create(kavlingPayload);
|
||||
}
|
||||
if(type==='kavling') await kavlingService.create(payload);
|
||||
window.showToast('Berhasil disimpan');
|
||||
setTimeout(()=>location.reload(), 800);
|
||||
} catch(e) { window.showToast('Gagal simpan', 'error'); }
|
||||
@@ -170,16 +90,3 @@ const handleGeometryCreated = (type, geometry, layer) => {
|
||||
appMap.removeLayer(tempLayer);
|
||||
});
|
||||
};
|
||||
|
||||
const init = async () => {
|
||||
appMap = initMap('map');
|
||||
setupCheckboxListeners();
|
||||
drawControl = setupDrawControls(appMap, handleGeometryCreated);
|
||||
await loadAllData();
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
@@ -2,23 +2,14 @@ export const renderForm = (type, geometry, onSubmit, onCancel) => {
|
||||
const container = document.getElementById('form-container');
|
||||
container.style.display = 'flex';
|
||||
|
||||
let extraFields = '';
|
||||
if (type === 'spbu') {
|
||||
extraFields = `
|
||||
<div class="form-group">
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" id="input-24jam">
|
||||
<span>Buka 24 Jam</span>
|
||||
</label>
|
||||
</div>
|
||||
`;
|
||||
} else if (type === 'kavling') {
|
||||
extraFields = `
|
||||
<div class="form-group">
|
||||
<input type="number" id="input-luas" class="form-control" placeholder="Luas (m²)" required min="1">
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
const extraFields = type === 'spbu' ? `
|
||||
<div class="form-group">
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" id="input-24jam">
|
||||
<span>Buka 24 Jam</span>
|
||||
</label>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="form-panel">
|
||||
@@ -45,7 +36,6 @@ export const renderForm = (type, geometry, onSubmit, onCancel) => {
|
||||
deskripsi: document.getElementById('input-deskripsi').value,
|
||||
};
|
||||
if (type === 'spbu') payload.buka_24_jam = document.getElementById('input-24jam').checked;
|
||||
if (type === 'kavling') payload.luas = document.getElementById('input-luas').value;
|
||||
|
||||
onSubmit(payload);
|
||||
container.style.display = 'none';
|
||||
|
||||
@@ -14,27 +14,12 @@ export const initMap = (containerId) => {
|
||||
position: 'bottomright'
|
||||
}).addTo(map);
|
||||
|
||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
const tileUrl = isDark
|
||||
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
|
||||
: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png';
|
||||
|
||||
// Tile Layer Premium (CartoDB Dark Matter / Light All)
|
||||
const tileLayer = L.tileLayer(tileUrl, {
|
||||
// Tile Layer Premium (CartoDB Dark Matter untuk Glass UI)
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
|
||||
subdomains: 'abcd',
|
||||
maxZoom: 20
|
||||
}).addTo(map);
|
||||
|
||||
// Dynamic theme updater for tile layer
|
||||
const observer = new MutationObserver(() => {
|
||||
const dark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
tileLayer.setUrl(dark
|
||||
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
|
||||
: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'
|
||||
);
|
||||
});
|
||||
observer.observe(document.documentElement, { attributes: true });
|
||||
|
||||
return map;
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
<?php
|
||||
/**
|
||||
* jalan.php
|
||||
* Tanggung Jawab: Menangani operasi CRUD untuk entitas Jalan (LineString).
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
||||
require_once '../core_config/database.php';
|
||||
require_once '../utils/response_helper.php';
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
<?php
|
||||
/**
|
||||
* kavling.php
|
||||
* Tanggung Jawab: Menangani operasi CRUD untuk entitas Kavling (Polygon).
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
||||
require_once '../core_config/database.php';
|
||||
require_once '../utils/response_helper.php';
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
@@ -21,7 +21,6 @@ switch ($method) {
|
||||
'geometry' => json_decode($row['geojson']),
|
||||
'properties' => [
|
||||
'id' => $row['id'],
|
||||
'nama' => $row['nama_pemilik'],
|
||||
'nama_pemilik' => $row['nama_pemilik'],
|
||||
'luas' => $row['luas'],
|
||||
'created_at' => $row['created_at']
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
<?php
|
||||
/**
|
||||
* rumah_ibadah.php
|
||||
* Tanggung Jawab: Operasi CRUD Rumah Ibadah + Endpoint analisis jangkauan Haversine.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
||||
require_once '../core_config/database.php';
|
||||
require_once '../utils/response_helper.php';
|
||||
require_once '../utils/geo_helper.php';
|
||||
|
||||
@@ -31,23 +31,7 @@ switch ($method) {
|
||||
|
||||
// Panggil geo_helper untuk mendapatkan warga miskin
|
||||
$warga = GeoHelper::getWargaDalamRadius($pdo, $pusatLat, $pusatLon, $radius);
|
||||
|
||||
// Format ke GeoJSON FeatureCollection sesuai kebutuhan frontend
|
||||
$features = [];
|
||||
foreach ($warga as $w) {
|
||||
$features[] = [
|
||||
'type' => 'Feature',
|
||||
'geometry' => $w['geometry'],
|
||||
'properties' => [
|
||||
'id' => $w['id'],
|
||||
'nama' => $w['nama_kk'],
|
||||
'penghasilan' => $w['penghasilan'],
|
||||
'jumlah_tanggungan' => $w['jumlah_tanggungan'],
|
||||
'jarak_km' => $w['jarak_km']
|
||||
]
|
||||
];
|
||||
}
|
||||
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Data jangkauan berhasil dihitung');
|
||||
sendSuccess($warga, 'Data jangkauan berhasil dihitung');
|
||||
|
||||
} else {
|
||||
// GET Semua Rumah Ibadah (GeoJSON)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
||||
<?php
|
||||
require_once '../core_config/database.php';
|
||||
require_once '../utils/response_helper.php';
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
<?php
|
||||
/**
|
||||
* warga_miskin.php
|
||||
* Tanggung Jawab: Operasi CRUD untuk Warga Miskin.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
||||
require_once '../core_config/database.php';
|
||||
require_once '../utils/response_helper.php';
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
|
||||
+87
-265
@@ -1,88 +1,33 @@
|
||||
/* ═══════════════════════════════════════════════════
|
||||
WebGIS P01 — main.css | Dark / Light Theme
|
||||
═══════════════════════════════════════════════════ */
|
||||
|
||||
/* ── Design Tokens ── */
|
||||
:root {
|
||||
--ease: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition: all 0.25s var(--ease);
|
||||
--primary: #6366F1;
|
||||
--primary-hover: #4F46E5;
|
||||
--bg-dark: #0F172A;
|
||||
--bg-glass: rgba(15, 23, 42, 0.75);
|
||||
--border-glass: rgba(255, 255, 255, 0.1);
|
||||
--text-light: #F8FAFC;
|
||||
--text-muted: #94A3B8;
|
||||
--radius: 20px;
|
||||
--shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
--font: 'Inter', sans-serif;
|
||||
--radius: 16px;
|
||||
}
|
||||
|
||||
/* ── Dark Theme (default) ── */
|
||||
[data-theme="dark"] {
|
||||
--primary: #818CF8;
|
||||
--primary-deep: #6366F1;
|
||||
--primary-glow: rgba(99,102,241,0.3);
|
||||
--bg-dark: #0A0F1E;
|
||||
--bg-glass: rgba(17,24,39,0.82);
|
||||
--bg-input: rgba(255,255,255,0.05);
|
||||
--border-glass: rgba(255,255,255,0.08);
|
||||
--border-focus: #6366F1;
|
||||
--text-main: #F1F5F9;
|
||||
--text-muted: #94A3B8;
|
||||
--shadow: 0 25px 50px -12px rgba(0,0,0,0.55);
|
||||
--dot-color: rgba(255,255,255,0.025);
|
||||
--btn-spbu: #10B981;
|
||||
--btn-jalan: #F59E0B;
|
||||
--btn-kavling: #3B82F6;
|
||||
--btn-rumah: #8B5CF6;
|
||||
--btn-warga: #EF4444;
|
||||
}
|
||||
|
||||
/* ── Light Theme ── */
|
||||
[data-theme="light"] {
|
||||
--primary: #4F46E5;
|
||||
--primary-deep: #3730A3;
|
||||
--primary-glow: rgba(79,70,229,0.15);
|
||||
--bg-dark: #F0F4FF;
|
||||
--bg-glass: rgba(255,255,255,0.92);
|
||||
--bg-input: #FFFFFF;
|
||||
--border-glass: #E2E8F0;
|
||||
--border-focus: #4F46E5;
|
||||
--text-main: #0F172A;
|
||||
--text-muted: #64748B;
|
||||
--shadow: 0 25px 50px -12px rgba(79,70,229,0.12);
|
||||
--dot-color: rgba(79,70,229,0.035);
|
||||
--btn-spbu: #059669;
|
||||
--btn-jalan: #D97706;
|
||||
--btn-kavling: #2563EB;
|
||||
--btn-rumah: #7C3AED;
|
||||
--btn-warga: #DC2626;
|
||||
}
|
||||
|
||||
/* ── Reset ── */
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--font);
|
||||
background: var(--bg-dark);
|
||||
color: var(--text-main);
|
||||
color: var(--text-light);
|
||||
overflow: hidden;
|
||||
transition: background 0.3s var(--ease), color 0.3s var(--ease);
|
||||
}
|
||||
|
||||
/* Dot grid background */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-image: radial-gradient(circle, var(--dot-color) 1px, transparent 1px);
|
||||
background-size: 28px 28px;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* ── Map ── */
|
||||
#map {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* ── Floating Dock ── */
|
||||
/* Floating Dock */
|
||||
.floating-dock {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
@@ -92,178 +37,90 @@ body::before {
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
border: 1px solid var(--border-glass);
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
width: 320px;
|
||||
border-radius: var(--radius);
|
||||
padding: 24px;
|
||||
width: 340px;
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: 20px;
|
||||
max-height: calc(100vh - 48px);
|
||||
overflow-y: auto;
|
||||
transition: background 0.3s var(--ease), border-color 0.3s var(--ease);
|
||||
}
|
||||
|
||||
.floating-dock::-webkit-scrollbar { width: 4px; }
|
||||
.floating-dock::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 10px; }
|
||||
/* Custom Scrollbar for Dock */
|
||||
.floating-dock::-webkit-scrollbar { width: 6px; }
|
||||
.floating-dock::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 10px; }
|
||||
|
||||
/* ── Dock Top Bar ── */
|
||||
.dock-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
.header h2 { margin: 0 0 8px 0; font-size: 1.4rem; font-weight: 700; background: linear-gradient(135deg, #818CF8, #C084FC); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
||||
.header p { margin: 0; font-size: 0.85rem; color: var(--text-muted); line-height: 1.5; }
|
||||
|
||||
.btn-back-portal {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-glass);
|
||||
background: var(--bg-input);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: var(--transition);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn-back-portal:hover {
|
||||
background: var(--primary-deep);
|
||||
border-color: var(--primary-deep);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dock-controls {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.btn-theme-dock {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-glass);
|
||||
background: var(--bg-input);
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.8rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
.btn-theme-dock:hover {
|
||||
background: var(--primary-deep);
|
||||
border-color: var(--primary-deep);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* ── Header ── */
|
||||
.header h2 {
|
||||
margin: 0 0 6px 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, var(--primary), #C084FC);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
.header p {
|
||||
margin: 0;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── Buttons ── */
|
||||
.menu-group { display: flex; flex-direction: column; gap: 8px; }
|
||||
.menu-title {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-muted);
|
||||
font-weight: 700;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
/* Buttons */
|
||||
.menu-group { display: flex; flex-direction: column; gap: 10px; }
|
||||
.menu-title { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1px; color: var(--text-muted); font-weight: 600; margin-bottom: 4px; }
|
||||
|
||||
.btn-action {
|
||||
background: var(--bg-input);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid var(--border-glass);
|
||||
color: var(--text-main);
|
||||
padding: 11px 14px;
|
||||
color: var(--text-light);
|
||||
padding: 14px 16px;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
font-family: var(--font);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
transition: var(--transition);
|
||||
font-size: 0.875rem;
|
||||
gap: 12px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-size: 0.9rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.btn-action svg { width: 18px; height: 18px; flex-shrink: 0; }
|
||||
.btn-action svg { width: 20px; height: 20px; flex-shrink: 0; }
|
||||
|
||||
.btn-action:hover {
|
||||
background: rgba(255,255,255,0.08);
|
||||
transform: translateX(3px);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
[data-theme="light"] .btn-action:hover {
|
||||
background: #EEF2FF;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.btn-action.active {
|
||||
background: var(--primary-deep);
|
||||
border-color: var(--primary-deep);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px var(--primary-glow);
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 10px 15px -3px rgba(99, 102, 241, 0.4);
|
||||
}
|
||||
|
||||
/* Button color variants */
|
||||
.btn-spbu svg { color: var(--btn-spbu); }
|
||||
.btn-jalan svg { color: var(--btn-jalan); }
|
||||
.btn-kavling svg { color: var(--btn-kavling); }
|
||||
.btn-rumah svg { color: var(--btn-rumah); }
|
||||
.btn-warga svg { color: var(--btn-warga); }
|
||||
/* Button Variants */
|
||||
.btn-spbu svg { color: #10B981; }
|
||||
.btn-jalan svg { color: #F59E0B; }
|
||||
.btn-kavling svg { color: #3B82F6; }
|
||||
.btn-rumah svg { color: #8B5CF6; }
|
||||
.btn-warga svg { color: #EF4444; }
|
||||
|
||||
/* ── Form Panel ── */
|
||||
/* Form Panel */
|
||||
.form-panel {
|
||||
background: rgba(0,0,0,0.3);
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--border-glass);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
[data-theme="light"] .form-panel {
|
||||
background: rgba(79,70,229,0.04);
|
||||
}
|
||||
|
||||
.form-group { display: flex; flex-direction: column; gap: 5px; }
|
||||
.form-group label { font-size: 0.78rem; color: var(--text-muted); font-weight: 500; }
|
||||
|
||||
.form-group { display: flex; flex-direction: column; gap: 6px; }
|
||||
.form-group label { font-size: 0.8rem; color: var(--text-muted); }
|
||||
.form-control {
|
||||
background: var(--bg-input);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid var(--border-glass);
|
||||
padding: 9px 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
color: var(--text-main);
|
||||
color: white;
|
||||
font-family: var(--font);
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.9rem;
|
||||
outline: none;
|
||||
transition: border 0.2s;
|
||||
}
|
||||
.form-control:focus { border-color: var(--border-focus); box-shadow: 0 0 0 3px var(--primary-glow); }
|
||||
.form-control::placeholder { color: var(--text-muted); }
|
||||
.form-control:focus { border-color: var(--primary); }
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
@@ -271,113 +128,78 @@ body::before {
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.checkbox-group input { width: 15px; height: 15px; accent-color: var(--primary); cursor: pointer; }
|
||||
.checkbox-group span { font-size: 0.82rem; color: var(--text-main); }
|
||||
.checkbox-group input { width: 16px; height: 16px; accent-color: var(--primary); cursor: pointer; }
|
||||
.checkbox-group span { font-size: 0.85rem; color: var(--text-light); }
|
||||
|
||||
.btn-submit {
|
||||
background: var(--primary-deep);
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 11px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-family: var(--font);
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
font-size: 0.875rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.btn-submit:hover { filter: brightness(1.1); transform: translateY(-1px); }
|
||||
.btn-submit:hover { background: var(--primary-hover); }
|
||||
|
||||
/* ── Leaflet Customizations ── */
|
||||
/* Leaflet Customizations */
|
||||
.leaflet-draw-toolbar { display: none !important; }
|
||||
.leaflet-control-zoom { border: none !important; box-shadow: var(--shadow) !important; }
|
||||
.leaflet-control-zoom a {
|
||||
background: var(--bg-glass) !important;
|
||||
color: var(--text-main) !important;
|
||||
color: var(--text-light) !important;
|
||||
border-color: var(--border-glass) !important;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.leaflet-control-zoom a:hover { background: var(--primary-deep) !important; color: white !important; }
|
||||
|
||||
/* Custom Leaflet Layer Control style to match theme */
|
||||
.leaflet-control-layers {
|
||||
background: var(--bg-glass) !important;
|
||||
backdrop-filter: blur(24px) !important;
|
||||
-webkit-backdrop-filter: blur(24px) !important;
|
||||
border: 1px solid var(--border-glass) !important;
|
||||
border-radius: 14px !important;
|
||||
box-shadow: var(--shadow) !important;
|
||||
color: var(--text-main) !important;
|
||||
font-family: var(--font) !important;
|
||||
padding: 10px 14px !important;
|
||||
transition: var(--transition);
|
||||
}
|
||||
.leaflet-control-layers-expanded {
|
||||
min-width: 140px;
|
||||
}
|
||||
.leaflet-control-layers label {
|
||||
margin-bottom: 6px !important;
|
||||
font-size: 0.85rem !important;
|
||||
cursor: pointer !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 8px !important;
|
||||
color: var(--text-main) !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
.leaflet-control-layers label:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.leaflet-control-layers input[type="checkbox"] {
|
||||
cursor: pointer !important;
|
||||
width: 15px !important;
|
||||
height: 15px !important;
|
||||
accent-color: var(--primary-deep) !important;
|
||||
}
|
||||
.leaflet-control-zoom a:hover { background: var(--primary) !important; }
|
||||
|
||||
/* Leaflet Popup Premium */
|
||||
.leaflet-popup-content-wrapper {
|
||||
background: var(--bg-glass) !important;
|
||||
backdrop-filter: blur(16px) !important;
|
||||
border: 1px solid var(--border-glass) !important;
|
||||
color: var(--text-main) !important;
|
||||
border-radius: 14px !important;
|
||||
color: var(--text-light) !important;
|
||||
border-radius: 16px !important;
|
||||
box-shadow: var(--shadow) !important;
|
||||
}
|
||||
.leaflet-popup-tip { background: var(--bg-glass) !important; }
|
||||
.leaflet-popup-content { margin: 14px !important; font-family: var(--font); }
|
||||
.leaflet-popup-content h3 { margin: 0 0 8px 0; font-size: 1rem; border-bottom: 1px solid var(--border-glass); padding-bottom: 8px; }
|
||||
.leaflet-popup-content p { margin: 4px 0; font-size: 0.85rem; color: var(--text-muted); }
|
||||
|
||||
.leaflet-popup-tip { background: var(--bg-glass) !important; border: 1px solid var(--border-glass) !important; }
|
||||
.leaflet-popup-content { margin: 16px !important; font-family: var(--font); }
|
||||
.leaflet-popup-content h3 { margin: 0 0 8px 0; font-size: 1.1rem; border-bottom: 1px solid var(--border-glass); padding-bottom: 8px; }
|
||||
.leaflet-popup-content p { margin: 4px 0; font-size: 0.9rem; color: var(--text-muted); }
|
||||
.leaflet-popup-content .btn-delete {
|
||||
background: rgba(239,68,68,0.1);
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #EF4444;
|
||||
border: 1px solid rgba(239,68,68,0.3);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
margin-top: 10px;
|
||||
margin-top: 12px;
|
||||
width: 100%;
|
||||
font-family: var(--font);
|
||||
font-size: 0.85rem;
|
||||
transition: var(--transition);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.leaflet-popup-content .btn-delete:hover { background: #EF4444; color: white; }
|
||||
|
||||
/* ── Toast ── */
|
||||
/* Custom Map Marker Icon Animation */
|
||||
.custom-icon div {
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.custom-icon:hover div {
|
||||
transform: scale(1.1) translateY(-4px);
|
||||
}
|
||||
|
||||
/* Toast Notification */
|
||||
.toast-container {
|
||||
position: fixed; bottom: 24px; right: 24px; z-index: 9999;
|
||||
display: flex; flex-direction: column; gap: 10px;
|
||||
}
|
||||
.toast {
|
||||
background: var(--bg-glass); backdrop-filter: blur(16px);
|
||||
border: 1px solid var(--border-glass); border-left: 4px solid var(--primary-deep);
|
||||
padding: 14px 18px; border-radius: 12px; color: var(--text-main); font-size: 0.875rem;
|
||||
box-shadow: var(--shadow); animation: slideIn 0.3s var(--ease);
|
||||
border: 1px solid var(--border-glass); border-left: 4px solid var(--primary);
|
||||
padding: 16px 20px; border-radius: 12px; color: white; font-size: 0.9rem;
|
||||
box-shadow: var(--shadow); animation: slideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.toast.error { border-left-color: #EF4444; }
|
||||
.toast.error { border-left-color: #EF4444; }
|
||||
.toast.success { border-left-color: #10B981; }
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
||||
|
||||
+16
-120
@@ -1,154 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id" data-theme="dark">
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebGIS — Pertemuan 02: Haversine & Tematik</title>
|
||||
<meta name="description" content="Modul 02 — Pemetaan sosial dan analisis radius jarak Haversine berbasis WebGIS.">
|
||||
<title>WebGIS - Pertemuan 02</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="css/main.css">
|
||||
<style>
|
||||
.result-item {
|
||||
background: var(--bg-input);
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-glass);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
.badge {
|
||||
background: var(--primary-deep);
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
.result-item { background: rgba(255,255,255,0.05); padding: 8px 10px; border-radius: 8px; border: 1px solid var(--border-glass); display: flex; justify-content: space-between; align-items: center; }
|
||||
.badge { background: var(--primary); padding: 2px 6px; border-radius: 12px; font-size: 0.7rem; font-weight: 700; color: white; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Map container -->
|
||||
<div id="map"></div>
|
||||
|
||||
<!-- Floating Dock -->
|
||||
<div class="floating-dock">
|
||||
|
||||
<!-- Dock top bar -->
|
||||
<div class="dock-topbar">
|
||||
<a href="../../portal.html" class="btn-back-portal" title="Kembali ke Portal">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
Portal
|
||||
</a>
|
||||
<div class="dock-controls">
|
||||
<button class="btn-theme-dock" id="themeToggle" title="Toggle tema" aria-label="Toggle dark/light mode">
|
||||
<i class="fas fa-moon" id="themeIcon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<h2>P02: Tematik & Jarak</h2>
|
||||
<h2>P02: Tematik & Jarak</h2>
|
||||
<p>Kumulatif P01 + Analisis radius jarak Haversine.</p>
|
||||
</div>
|
||||
|
||||
<!-- Layer Peta (Centang) -->
|
||||
<div class="menu-group">
|
||||
<div class="menu-title">Layer Peta</div>
|
||||
<div class="form-panel" style="gap: 10px;">
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" id="layer-spbu" checked>
|
||||
<span>Stasiun SPBU</span>
|
||||
</label>
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" id="layer-jalan" checked>
|
||||
<span>Jalur Jalan</span>
|
||||
</label>
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" id="layer-kavling" checked>
|
||||
<span>Kavling Tanah</span>
|
||||
</label>
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" id="layer-rumah" checked>
|
||||
<span>Rumah Ibadah</span>
|
||||
</label>
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" id="layer-warga" checked>
|
||||
<span>Warga Miskin</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Entitas Dasar (P01) -->
|
||||
<div class="menu-group">
|
||||
<div class="menu-title">Entitas Dasar (P01)</div>
|
||||
<button id="btn-draw-spbu" class="btn-action btn-spbu">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.242-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
||||
Tambah SPBU
|
||||
</button>
|
||||
<button id="btn-draw-jalan" class="btn-action btn-jalan">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path></svg>
|
||||
Tambah Jalan
|
||||
</button>
|
||||
<button id="btn-draw-kavling" class="btn-action btn-kavling">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path></svg>
|
||||
Tambah Kavling
|
||||
</button>
|
||||
<button id="btn-draw-spbu" class="btn-action btn-spbu"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.242-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg> Tambah SPBU</button>
|
||||
<button id="btn-draw-jalan" class="btn-action btn-jalan"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path></svg> Tambah Jalan</button>
|
||||
<button id="btn-draw-kavling" class="btn-action btn-kavling"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path></svg> Tambah Kavling</button>
|
||||
</div>
|
||||
|
||||
<!-- Entitas Sosial (P02) -->
|
||||
<div class="menu-group" style="margin-top:4px;">
|
||||
<div class="menu-group" style="margin-top: 5px;">
|
||||
<div class="menu-title">Entitas Sosial (P02)</div>
|
||||
<button id="btn-draw-rumah" class="btn-action btn-rumah">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path></svg>
|
||||
Tambah Rumah Ibadah
|
||||
</button>
|
||||
<button id="btn-draw-warga" class="btn-action btn-warga">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path></svg>
|
||||
Tambah Warga Miskin
|
||||
</button>
|
||||
<button id="btn-draw-rumah" class="btn-action btn-rumah"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path></svg> Tambah Rumah Ibadah</button>
|
||||
<button id="btn-draw-warga" class="btn-action btn-warga"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path></svg> Tambah Warga Miskin</button>
|
||||
</div>
|
||||
|
||||
<!-- Analisis Jarak -->
|
||||
<div id="instruction-panel" class="form-panel" style="margin-top:2px;">
|
||||
<h3 style="margin:0 0 4px 0; font-size:0.85rem; color:var(--text-main); font-weight:600;">Analisis Jarak</h3>
|
||||
<p style="font-size:0.78rem; margin:0 0 8px 0; color:var(--text-muted);">Klik [Cek Radius] di popup Rumah Ibadah mana pun.</p>
|
||||
<div id="radius-results" style="max-height:140px; overflow-y:auto; display:flex; flex-direction:column; gap:6px;"></div>
|
||||
<div id="instruction-panel" class="form-panel" style="margin-top: 5px;">
|
||||
<h3 style="margin:0 0 5px 0; font-size:0.9rem; color:var(--text-main);">Analisis Jarak</h3>
|
||||
<p style="font-size:0.8rem; margin:0 0 10px 0; color:var(--text-muted);">Klik [Cek Radius] di popup Rumah Ibadah mana pun.</p>
|
||||
<div id="radius-results" style="max-height:150px; overflow-y:auto; display:flex; flex-direction:column; gap:8px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic form container -->
|
||||
<div id="form-container" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<div id="toast-container" class="toast-container"></div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
|
||||
<script>
|
||||
// ── Theme toggle ──
|
||||
const html = document.documentElement;
|
||||
const btn = document.getElementById('themeToggle');
|
||||
const icon = document.getElementById('themeIcon');
|
||||
const saved = localStorage.getItem('portal-theme') || 'dark';
|
||||
setTheme(saved);
|
||||
btn.addEventListener('click', () => {
|
||||
const next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
||||
setTheme(next);
|
||||
localStorage.setItem('portal-theme', next);
|
||||
});
|
||||
function setTheme(t) {
|
||||
html.setAttribute('data-theme', t);
|
||||
icon.className = t === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
|
||||
}
|
||||
</script>
|
||||
<script type="module" src="js/app.js?v=20260613-9"></script>
|
||||
|
||||
<script type="module" src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+17
-52
@@ -29,6 +29,17 @@ window.showToast = (msg, type='success') => {
|
||||
setTimeout(() => { t.style.transform='translateX(100%)'; t.style.opacity=0; setTimeout(()=>t.remove(),300); }, 3000);
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
appMap = initMap('map');
|
||||
|
||||
// Bind extra draw events for 02
|
||||
document.getElementById('btn-draw-rumah')?.addEventListener('click', () => { window.currentDrawType='rumah_ibadah'; new L.Draw.Marker(appMap).enable(); });
|
||||
document.getElementById('btn-draw-warga')?.addEventListener('click', () => { window.currentDrawType='warga_miskin'; new L.Draw.Marker(appMap).enable(); });
|
||||
|
||||
drawControl = setupDrawControls(appMap, handleGeometryCreated);
|
||||
await loadAllData();
|
||||
});
|
||||
|
||||
const loadAllData = async () => {
|
||||
try {
|
||||
const [spbu, jalan, kavling, rumah, warga] = await Promise.all([
|
||||
@@ -36,33 +47,11 @@ const loadAllData = async () => {
|
||||
rumahIbadahService.getAll(), wargaMiskinService.getAll()
|
||||
]);
|
||||
|
||||
const spbuLayer = L.geoJSON(spbu, { pointToLayer: (f, ll) => L.marker(ll, { icon: iconSPBU(f.properties.buka_24_jam) }), onEachFeature: (f, l) => bindPopup(l, 'spbu', f.properties) }).addTo(appMap);
|
||||
const jalanLayer = L.geoJSON(jalan, { style: { color: '#F59E0B', weight: 4 }, onEachFeature: (f, l) => bindPopup(l, 'jalan', f.properties) }).addTo(appMap);
|
||||
const kavlingLayer = L.geoJSON(kavling, { style: { color: '#3B82F6', weight: 2, fillColor: '#3B82F6', fillOpacity: 0.3 }, onEachFeature: (f, l) => bindPopup(l, 'kavling', f.properties) }).addTo(appMap);
|
||||
const rumahLayer = L.geoJSON(rumah, { pointToLayer: (f, ll) => L.marker(ll, { icon: iconRumah }), onEachFeature: (f, l) => bindPopup(l, 'rumah_ibadah', f.properties, f) }).addTo(appMap);
|
||||
const wargaLayer = L.geoJSON(warga, { pointToLayer: (f, ll) => L.marker(ll, { icon: iconWarga }), onEachFeature: (f, l) => bindPopup(l, 'warga_miskin', f.properties) }).addTo(appMap);
|
||||
|
||||
// Bind checkbox events
|
||||
document.getElementById('layer-spbu').addEventListener('change', (e) => {
|
||||
if (e.target.checked) appMap.addLayer(spbuLayer);
|
||||
else appMap.removeLayer(spbuLayer);
|
||||
});
|
||||
document.getElementById('layer-jalan').addEventListener('change', (e) => {
|
||||
if (e.target.checked) appMap.addLayer(jalanLayer);
|
||||
else appMap.removeLayer(jalanLayer);
|
||||
});
|
||||
document.getElementById('layer-kavling').addEventListener('change', (e) => {
|
||||
if (e.target.checked) appMap.addLayer(kavlingLayer);
|
||||
else appMap.removeLayer(kavlingLayer);
|
||||
});
|
||||
document.getElementById('layer-rumah').addEventListener('change', (e) => {
|
||||
if (e.target.checked) appMap.addLayer(rumahLayer);
|
||||
else appMap.removeLayer(rumahLayer);
|
||||
});
|
||||
document.getElementById('layer-warga').addEventListener('change', (e) => {
|
||||
if (e.target.checked) appMap.addLayer(wargaLayer);
|
||||
else appMap.removeLayer(wargaLayer);
|
||||
});
|
||||
L.geoJSON(spbu, { pointToLayer: (f, ll) => L.marker(ll, { icon: iconSPBU(f.properties.buka_24_jam) }), onEachFeature: (f, l) => bindPopup(l, 'spbu', f.properties) }).addTo(appMap);
|
||||
L.geoJSON(jalan, { style: { color: '#F59E0B', weight: 4 }, onEachFeature: (f, l) => bindPopup(l, 'jalan', f.properties) }).addTo(appMap);
|
||||
L.geoJSON(kavling, { style: { color: '#3B82F6', weight: 2, fillColor: '#3B82F6', fillOpacity: 0.3 }, onEachFeature: (f, l) => bindPopup(l, 'kavling', f.properties) }).addTo(appMap);
|
||||
L.geoJSON(rumah, { pointToLayer: (f, ll) => L.marker(ll, { icon: iconRumah }), onEachFeature: (f, l) => bindPopup(l, 'rumah_ibadah', f.properties, f) }).addTo(appMap);
|
||||
L.geoJSON(warga, { pointToLayer: (f, ll) => L.marker(ll, { icon: iconWarga }), onEachFeature: (f, l) => bindPopup(l, 'warga_miskin', f.properties) }).addTo(appMap);
|
||||
} catch (e) {
|
||||
window.showToast("Gagal meload data: "+e.message, 'error');
|
||||
}
|
||||
@@ -126,34 +115,10 @@ const handleGeometryCreated = (type, geometry, layer) => {
|
||||
try {
|
||||
if(type==='spbu') await spbuService.create(payload);
|
||||
if(type==='jalan') await jalanService.create(payload);
|
||||
if(type==='kavling') {
|
||||
const kavlingPayload = {
|
||||
nama_pemilik: payload.nama,
|
||||
luas: payload.luas || 0,
|
||||
geometry: payload.geometry
|
||||
};
|
||||
await kavlingService.create(kavlingPayload);
|
||||
}
|
||||
if(type==='kavling') await kavlingService.create(payload);
|
||||
if(type==='rumah_ibadah') await rumahIbadahService.create(payload);
|
||||
if(type==='warga_miskin') await wargaMiskinService.create(payload);
|
||||
window.showToast('Tersimpan'); setTimeout(()=>location.reload(), 800);
|
||||
} catch(e) { window.showToast('Gagal simpan', 'error'); }
|
||||
}, () => appMap.removeLayer(tempLayer));
|
||||
};
|
||||
|
||||
const init = async () => {
|
||||
appMap = initMap('map');
|
||||
|
||||
// Bind extra draw events for 02
|
||||
document.getElementById('btn-draw-rumah')?.addEventListener('click', () => { window.currentDrawType='rumah_ibadah'; new L.Draw.Marker(appMap).enable(); });
|
||||
document.getElementById('btn-draw-warga')?.addEventListener('click', () => { window.currentDrawType='warga_miskin'; new L.Draw.Marker(appMap).enable(); });
|
||||
|
||||
drawControl = setupDrawControls(appMap, handleGeometryCreated);
|
||||
await loadAllData();
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
@@ -14,27 +14,12 @@ export const initMap = (containerId) => {
|
||||
position: 'bottomright'
|
||||
}).addTo(map);
|
||||
|
||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
const tileUrl = isDark
|
||||
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
|
||||
: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png';
|
||||
|
||||
// Tile Layer Premium (CartoDB Dark Matter / Light All)
|
||||
const tileLayer = L.tileLayer(tileUrl, {
|
||||
// Tile Layer Premium (CartoDB Dark Matter untuk Glass UI)
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
|
||||
subdomains: 'abcd',
|
||||
maxZoom: 20
|
||||
}).addTo(map);
|
||||
|
||||
// Dynamic theme updater for tile layer
|
||||
const observer = new MutationObserver(() => {
|
||||
const dark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
tileLayer.setUrl(dark
|
||||
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
|
||||
: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'
|
||||
);
|
||||
});
|
||||
observer.observe(document.documentElement, { attributes: true });
|
||||
|
||||
return map;
|
||||
};
|
||||
|
||||
@@ -42,7 +42,7 @@ export const rumahIbadahService = createService('02/backend/api/rumah_ibadah.php
|
||||
export const wargaMiskinService = createService('02/backend/api/warga_miskin.php');
|
||||
export const haversineService = {
|
||||
getDalamRadius: async (id, radius) => {
|
||||
const res = await fetch(`${BASE_URL}/02/backend/api/rumah_ibadah.php?action=jangkauan&id=${id}&radius=${radius}`);
|
||||
const res = await fetch(`${BASE_URL}/02/backend/api/haversine.php?rumah_ibadah_id=${id}&radius_km=${radius}`);
|
||||
const json = await res.json();
|
||||
return json.data;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
<?php
|
||||
/**
|
||||
* jalan.php
|
||||
* Tanggung Jawab: Menangani operasi CRUD untuk entitas Jalan (LineString).
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
||||
require_once '../core_config/database.php';
|
||||
require_once '../utils/response_helper.php';
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
<?php
|
||||
/**
|
||||
* kavling.php
|
||||
* Tanggung Jawab: Menangani operasi CRUD untuk entitas Kavling (Polygon).
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
||||
require_once '../core_config/database.php';
|
||||
require_once '../utils/response_helper.php';
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
@@ -21,7 +21,6 @@ switch ($method) {
|
||||
'geometry' => json_decode($row['geojson']),
|
||||
'properties' => [
|
||||
'id' => $row['id'],
|
||||
'nama' => $row['nama_pemilik'],
|
||||
'nama_pemilik' => $row['nama_pemilik'],
|
||||
'luas' => $row['luas'],
|
||||
'created_at' => $row['created_at']
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
<?php
|
||||
/**
|
||||
* rumah_ibadah.php
|
||||
* Tanggung Jawab: Operasi CRUD Rumah Ibadah + Endpoint analisis jangkauan Haversine.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
||||
require_once '../core_config/database.php';
|
||||
require_once '../utils/response_helper.php';
|
||||
require_once '../utils/geo_helper.php';
|
||||
|
||||
@@ -31,23 +31,7 @@ switch ($method) {
|
||||
|
||||
// Panggil geo_helper untuk mendapatkan warga miskin
|
||||
$warga = GeoHelper::getWargaDalamRadius($pdo, $pusatLat, $pusatLon, $radius);
|
||||
|
||||
// Format ke GeoJSON FeatureCollection sesuai kebutuhan frontend
|
||||
$features = [];
|
||||
foreach ($warga as $w) {
|
||||
$features[] = [
|
||||
'type' => 'Feature',
|
||||
'geometry' => $w['geometry'],
|
||||
'properties' => [
|
||||
'id' => $w['id'],
|
||||
'nama' => $w['nama_kk'],
|
||||
'penghasilan' => $w['penghasilan'],
|
||||
'jumlah_tanggungan' => $w['jumlah_tanggungan'],
|
||||
'jarak_km' => $w['jarak_km']
|
||||
]
|
||||
];
|
||||
}
|
||||
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Data jangkauan berhasil dihitung');
|
||||
sendSuccess($warga, 'Data jangkauan berhasil dihitung');
|
||||
|
||||
} else {
|
||||
// GET Semua Rumah Ibadah (GeoJSON)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
||||
<?php
|
||||
require_once '../core_config/database.php';
|
||||
require_once '../utils/response_helper.php';
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<?php
|
||||
<?php
|
||||
/**
|
||||
* statistik.php
|
||||
* Tanggung Jawab: Menyediakan data analisis spasial menggunakan Point in Polygon (ST_Contains).
|
||||
* Menghitung kepadatan warga miskin dalam setiap kavling.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
||||
require_once '../core_config/database.php';
|
||||
require_once '../utils/response_helper.php';
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
@@ -31,25 +31,37 @@ if ($method === 'GET') {
|
||||
$stmt = $pdo->query($sql);
|
||||
|
||||
$features = [];
|
||||
$total_warga = 0;
|
||||
|
||||
while ($row = $stmt->fetch()) {
|
||||
$jumlah = (int) $row['jumlah_warga'];
|
||||
$total_warga += $jumlah;
|
||||
|
||||
$features[] = [
|
||||
'type' => 'Feature',
|
||||
'geometry' => json_decode($row['geojson']),
|
||||
'properties' => [
|
||||
'id' => $row['id'],
|
||||
'nama' => $row['nama_pemilik'],
|
||||
'nama_pemilik' => $row['nama_pemilik'],
|
||||
'luas' => $row['luas'],
|
||||
'jumlah_warga_miskin' => $jumlah,
|
||||
'jumlah_warga' => $jumlah,
|
||||
'total_tanggungan' => (int) $row['total_tanggungan']
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Data statistik spasial berhasil dihitung');
|
||||
$response = [
|
||||
'statistik' => [
|
||||
'total_kavling' => count($features),
|
||||
'total_warga_terpetakan' => $total_warga
|
||||
],
|
||||
'geojson' => [
|
||||
'type' => 'FeatureCollection',
|
||||
'features' => $features
|
||||
]
|
||||
];
|
||||
|
||||
sendSuccess($response, 'Data statistik spasial berhasil dihitung');
|
||||
} catch (PDOException $e) {
|
||||
sendError('Gagal menghitung statistik spasial: ' . $e->getMessage(), 500);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
<?php
|
||||
/**
|
||||
* warga_miskin.php
|
||||
* Tanggung Jawab: Operasi CRUD untuk Warga Miskin.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
||||
require_once '../core_config/database.php';
|
||||
require_once '../utils/response_helper.php';
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
|
||||
+87
-265
@@ -1,88 +1,33 @@
|
||||
/* ═══════════════════════════════════════════════════
|
||||
WebGIS P01 — main.css | Dark / Light Theme
|
||||
═══════════════════════════════════════════════════ */
|
||||
|
||||
/* ── Design Tokens ── */
|
||||
:root {
|
||||
--ease: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition: all 0.25s var(--ease);
|
||||
--primary: #6366F1;
|
||||
--primary-hover: #4F46E5;
|
||||
--bg-dark: #0F172A;
|
||||
--bg-glass: rgba(15, 23, 42, 0.75);
|
||||
--border-glass: rgba(255, 255, 255, 0.1);
|
||||
--text-light: #F8FAFC;
|
||||
--text-muted: #94A3B8;
|
||||
--radius: 20px;
|
||||
--shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
--font: 'Inter', sans-serif;
|
||||
--radius: 16px;
|
||||
}
|
||||
|
||||
/* ── Dark Theme (default) ── */
|
||||
[data-theme="dark"] {
|
||||
--primary: #818CF8;
|
||||
--primary-deep: #6366F1;
|
||||
--primary-glow: rgba(99,102,241,0.3);
|
||||
--bg-dark: #0A0F1E;
|
||||
--bg-glass: rgba(17,24,39,0.82);
|
||||
--bg-input: rgba(255,255,255,0.05);
|
||||
--border-glass: rgba(255,255,255,0.08);
|
||||
--border-focus: #6366F1;
|
||||
--text-main: #F1F5F9;
|
||||
--text-muted: #94A3B8;
|
||||
--shadow: 0 25px 50px -12px rgba(0,0,0,0.55);
|
||||
--dot-color: rgba(255,255,255,0.025);
|
||||
--btn-spbu: #10B981;
|
||||
--btn-jalan: #F59E0B;
|
||||
--btn-kavling: #3B82F6;
|
||||
--btn-rumah: #8B5CF6;
|
||||
--btn-warga: #EF4444;
|
||||
}
|
||||
|
||||
/* ── Light Theme ── */
|
||||
[data-theme="light"] {
|
||||
--primary: #4F46E5;
|
||||
--primary-deep: #3730A3;
|
||||
--primary-glow: rgba(79,70,229,0.15);
|
||||
--bg-dark: #F0F4FF;
|
||||
--bg-glass: rgba(255,255,255,0.92);
|
||||
--bg-input: #FFFFFF;
|
||||
--border-glass: #E2E8F0;
|
||||
--border-focus: #4F46E5;
|
||||
--text-main: #0F172A;
|
||||
--text-muted: #64748B;
|
||||
--shadow: 0 25px 50px -12px rgba(79,70,229,0.12);
|
||||
--dot-color: rgba(79,70,229,0.035);
|
||||
--btn-spbu: #059669;
|
||||
--btn-jalan: #D97706;
|
||||
--btn-kavling: #2563EB;
|
||||
--btn-rumah: #7C3AED;
|
||||
--btn-warga: #DC2626;
|
||||
}
|
||||
|
||||
/* ── Reset ── */
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--font);
|
||||
background: var(--bg-dark);
|
||||
color: var(--text-main);
|
||||
color: var(--text-light);
|
||||
overflow: hidden;
|
||||
transition: background 0.3s var(--ease), color 0.3s var(--ease);
|
||||
}
|
||||
|
||||
/* Dot grid background */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-image: radial-gradient(circle, var(--dot-color) 1px, transparent 1px);
|
||||
background-size: 28px 28px;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* ── Map ── */
|
||||
#map {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* ── Floating Dock ── */
|
||||
/* Floating Dock */
|
||||
.floating-dock {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
@@ -92,178 +37,90 @@ body::before {
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
border: 1px solid var(--border-glass);
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
width: 320px;
|
||||
border-radius: var(--radius);
|
||||
padding: 24px;
|
||||
width: 340px;
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: 20px;
|
||||
max-height: calc(100vh - 48px);
|
||||
overflow-y: auto;
|
||||
transition: background 0.3s var(--ease), border-color 0.3s var(--ease);
|
||||
}
|
||||
|
||||
.floating-dock::-webkit-scrollbar { width: 4px; }
|
||||
.floating-dock::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 10px; }
|
||||
/* Custom Scrollbar for Dock */
|
||||
.floating-dock::-webkit-scrollbar { width: 6px; }
|
||||
.floating-dock::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 10px; }
|
||||
|
||||
/* ── Dock Top Bar ── */
|
||||
.dock-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
.header h2 { margin: 0 0 8px 0; font-size: 1.4rem; font-weight: 700; background: linear-gradient(135deg, #818CF8, #C084FC); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
||||
.header p { margin: 0; font-size: 0.85rem; color: var(--text-muted); line-height: 1.5; }
|
||||
|
||||
.btn-back-portal {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-glass);
|
||||
background: var(--bg-input);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: var(--transition);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn-back-portal:hover {
|
||||
background: var(--primary-deep);
|
||||
border-color: var(--primary-deep);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dock-controls {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.btn-theme-dock {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-glass);
|
||||
background: var(--bg-input);
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.8rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
.btn-theme-dock:hover {
|
||||
background: var(--primary-deep);
|
||||
border-color: var(--primary-deep);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* ── Header ── */
|
||||
.header h2 {
|
||||
margin: 0 0 6px 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, var(--primary), #C084FC);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
.header p {
|
||||
margin: 0;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── Buttons ── */
|
||||
.menu-group { display: flex; flex-direction: column; gap: 8px; }
|
||||
.menu-title {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-muted);
|
||||
font-weight: 700;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
/* Buttons */
|
||||
.menu-group { display: flex; flex-direction: column; gap: 10px; }
|
||||
.menu-title { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1px; color: var(--text-muted); font-weight: 600; margin-bottom: 4px; }
|
||||
|
||||
.btn-action {
|
||||
background: var(--bg-input);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid var(--border-glass);
|
||||
color: var(--text-main);
|
||||
padding: 11px 14px;
|
||||
color: var(--text-light);
|
||||
padding: 14px 16px;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
font-family: var(--font);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
transition: var(--transition);
|
||||
font-size: 0.875rem;
|
||||
gap: 12px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-size: 0.9rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.btn-action svg { width: 18px; height: 18px; flex-shrink: 0; }
|
||||
.btn-action svg { width: 20px; height: 20px; flex-shrink: 0; }
|
||||
|
||||
.btn-action:hover {
|
||||
background: rgba(255,255,255,0.08);
|
||||
transform: translateX(3px);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
[data-theme="light"] .btn-action:hover {
|
||||
background: #EEF2FF;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.btn-action.active {
|
||||
background: var(--primary-deep);
|
||||
border-color: var(--primary-deep);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px var(--primary-glow);
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 10px 15px -3px rgba(99, 102, 241, 0.4);
|
||||
}
|
||||
|
||||
/* Button color variants */
|
||||
.btn-spbu svg { color: var(--btn-spbu); }
|
||||
.btn-jalan svg { color: var(--btn-jalan); }
|
||||
.btn-kavling svg { color: var(--btn-kavling); }
|
||||
.btn-rumah svg { color: var(--btn-rumah); }
|
||||
.btn-warga svg { color: var(--btn-warga); }
|
||||
/* Button Variants */
|
||||
.btn-spbu svg { color: #10B981; }
|
||||
.btn-jalan svg { color: #F59E0B; }
|
||||
.btn-kavling svg { color: #3B82F6; }
|
||||
.btn-rumah svg { color: #8B5CF6; }
|
||||
.btn-warga svg { color: #EF4444; }
|
||||
|
||||
/* ── Form Panel ── */
|
||||
/* Form Panel */
|
||||
.form-panel {
|
||||
background: rgba(0,0,0,0.3);
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--border-glass);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
[data-theme="light"] .form-panel {
|
||||
background: rgba(79,70,229,0.04);
|
||||
}
|
||||
|
||||
.form-group { display: flex; flex-direction: column; gap: 5px; }
|
||||
.form-group label { font-size: 0.78rem; color: var(--text-muted); font-weight: 500; }
|
||||
|
||||
.form-group { display: flex; flex-direction: column; gap: 6px; }
|
||||
.form-group label { font-size: 0.8rem; color: var(--text-muted); }
|
||||
.form-control {
|
||||
background: var(--bg-input);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid var(--border-glass);
|
||||
padding: 9px 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
color: var(--text-main);
|
||||
color: white;
|
||||
font-family: var(--font);
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.9rem;
|
||||
outline: none;
|
||||
transition: border 0.2s;
|
||||
}
|
||||
.form-control:focus { border-color: var(--border-focus); box-shadow: 0 0 0 3px var(--primary-glow); }
|
||||
.form-control::placeholder { color: var(--text-muted); }
|
||||
.form-control:focus { border-color: var(--primary); }
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
@@ -271,113 +128,78 @@ body::before {
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.checkbox-group input { width: 15px; height: 15px; accent-color: var(--primary); cursor: pointer; }
|
||||
.checkbox-group span { font-size: 0.82rem; color: var(--text-main); }
|
||||
.checkbox-group input { width: 16px; height: 16px; accent-color: var(--primary); cursor: pointer; }
|
||||
.checkbox-group span { font-size: 0.85rem; color: var(--text-light); }
|
||||
|
||||
.btn-submit {
|
||||
background: var(--primary-deep);
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 11px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-family: var(--font);
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
font-size: 0.875rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.btn-submit:hover { filter: brightness(1.1); transform: translateY(-1px); }
|
||||
.btn-submit:hover { background: var(--primary-hover); }
|
||||
|
||||
/* ── Leaflet Customizations ── */
|
||||
/* Leaflet Customizations */
|
||||
.leaflet-draw-toolbar { display: none !important; }
|
||||
.leaflet-control-zoom { border: none !important; box-shadow: var(--shadow) !important; }
|
||||
.leaflet-control-zoom a {
|
||||
background: var(--bg-glass) !important;
|
||||
color: var(--text-main) !important;
|
||||
color: var(--text-light) !important;
|
||||
border-color: var(--border-glass) !important;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.leaflet-control-zoom a:hover { background: var(--primary-deep) !important; color: white !important; }
|
||||
|
||||
/* Custom Leaflet Layer Control style to match theme */
|
||||
.leaflet-control-layers {
|
||||
background: var(--bg-glass) !important;
|
||||
backdrop-filter: blur(24px) !important;
|
||||
-webkit-backdrop-filter: blur(24px) !important;
|
||||
border: 1px solid var(--border-glass) !important;
|
||||
border-radius: 14px !important;
|
||||
box-shadow: var(--shadow) !important;
|
||||
color: var(--text-main) !important;
|
||||
font-family: var(--font) !important;
|
||||
padding: 10px 14px !important;
|
||||
transition: var(--transition);
|
||||
}
|
||||
.leaflet-control-layers-expanded {
|
||||
min-width: 140px;
|
||||
}
|
||||
.leaflet-control-layers label {
|
||||
margin-bottom: 6px !important;
|
||||
font-size: 0.85rem !important;
|
||||
cursor: pointer !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 8px !important;
|
||||
color: var(--text-main) !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
.leaflet-control-layers label:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.leaflet-control-layers input[type="checkbox"] {
|
||||
cursor: pointer !important;
|
||||
width: 15px !important;
|
||||
height: 15px !important;
|
||||
accent-color: var(--primary-deep) !important;
|
||||
}
|
||||
.leaflet-control-zoom a:hover { background: var(--primary) !important; }
|
||||
|
||||
/* Leaflet Popup Premium */
|
||||
.leaflet-popup-content-wrapper {
|
||||
background: var(--bg-glass) !important;
|
||||
backdrop-filter: blur(16px) !important;
|
||||
border: 1px solid var(--border-glass) !important;
|
||||
color: var(--text-main) !important;
|
||||
border-radius: 14px !important;
|
||||
color: var(--text-light) !important;
|
||||
border-radius: 16px !important;
|
||||
box-shadow: var(--shadow) !important;
|
||||
}
|
||||
.leaflet-popup-tip { background: var(--bg-glass) !important; }
|
||||
.leaflet-popup-content { margin: 14px !important; font-family: var(--font); }
|
||||
.leaflet-popup-content h3 { margin: 0 0 8px 0; font-size: 1rem; border-bottom: 1px solid var(--border-glass); padding-bottom: 8px; }
|
||||
.leaflet-popup-content p { margin: 4px 0; font-size: 0.85rem; color: var(--text-muted); }
|
||||
|
||||
.leaflet-popup-tip { background: var(--bg-glass) !important; border: 1px solid var(--border-glass) !important; }
|
||||
.leaflet-popup-content { margin: 16px !important; font-family: var(--font); }
|
||||
.leaflet-popup-content h3 { margin: 0 0 8px 0; font-size: 1.1rem; border-bottom: 1px solid var(--border-glass); padding-bottom: 8px; }
|
||||
.leaflet-popup-content p { margin: 4px 0; font-size: 0.9rem; color: var(--text-muted); }
|
||||
.leaflet-popup-content .btn-delete {
|
||||
background: rgba(239,68,68,0.1);
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #EF4444;
|
||||
border: 1px solid rgba(239,68,68,0.3);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
margin-top: 10px;
|
||||
margin-top: 12px;
|
||||
width: 100%;
|
||||
font-family: var(--font);
|
||||
font-size: 0.85rem;
|
||||
transition: var(--transition);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.leaflet-popup-content .btn-delete:hover { background: #EF4444; color: white; }
|
||||
|
||||
/* ── Toast ── */
|
||||
/* Custom Map Marker Icon Animation */
|
||||
.custom-icon div {
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.custom-icon:hover div {
|
||||
transform: scale(1.1) translateY(-4px);
|
||||
}
|
||||
|
||||
/* Toast Notification */
|
||||
.toast-container {
|
||||
position: fixed; bottom: 24px; right: 24px; z-index: 9999;
|
||||
display: flex; flex-direction: column; gap: 10px;
|
||||
}
|
||||
.toast {
|
||||
background: var(--bg-glass); backdrop-filter: blur(16px);
|
||||
border: 1px solid var(--border-glass); border-left: 4px solid var(--primary-deep);
|
||||
padding: 14px 18px; border-radius: 12px; color: var(--text-main); font-size: 0.875rem;
|
||||
box-shadow: var(--shadow); animation: slideIn 0.3s var(--ease);
|
||||
border: 1px solid var(--border-glass); border-left: 4px solid var(--primary);
|
||||
padding: 16px 20px; border-radius: 12px; color: white; font-size: 0.9rem;
|
||||
box-shadow: var(--shadow); animation: slideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.toast.error { border-left-color: #EF4444; }
|
||||
.toast.error { border-left-color: #EF4444; }
|
||||
.toast.success { border-left-color: #10B981; }
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
||||
|
||||
+19
-131
@@ -1,165 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id" data-theme="dark">
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebGIS — Pertemuan 03: Analisis Spasial</title>
|
||||
<meta name="description" content="Modul 03 — Choropleth Map dinamis berbasis Point-in-Polygon berbasis WebGIS.">
|
||||
<title>WebGIS - Pertemuan 03</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="css/main.css">
|
||||
<style>
|
||||
.result-item {
|
||||
background: var(--bg-input);
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-glass);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
.badge {
|
||||
background: var(--primary-deep);
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
.stat-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
.stat-row strong { font-weight: 700; }
|
||||
.result-item { background: rgba(255,255,255,0.05); padding: 8px 10px; border-radius: 8px; border: 1px solid var(--border-glass); display: flex; justify-content: space-between; align-items: center; }
|
||||
.badge { background: var(--primary); padding: 2px 6px; border-radius: 12px; font-size: 0.7rem; font-weight: 700; color: white; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Map container -->
|
||||
<div id="map"></div>
|
||||
|
||||
<!-- Floating Dock -->
|
||||
<div class="floating-dock">
|
||||
|
||||
<!-- Dock top bar -->
|
||||
<div class="dock-topbar">
|
||||
<a href="../../portal.html" class="btn-back-portal" title="Kembali ke Portal">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
Portal
|
||||
</a>
|
||||
<div class="dock-controls">
|
||||
<button class="btn-theme-dock" id="themeToggle" title="Toggle tema" aria-label="Toggle dark/light mode">
|
||||
<i class="fas fa-moon" id="themeIcon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<h2>P03: Analisis Spasial</h2>
|
||||
<p>Kumulatif P01 & P02 + Choropleth Map (Point-in-Polygon).</p>
|
||||
<p>Kumulatif P01 & P02 + Choropleth Map (Point in Polygon).</p>
|
||||
</div>
|
||||
|
||||
<!-- Layer Peta (Centang) -->
|
||||
<div class="menu-group">
|
||||
<div class="menu-title">Layer Peta</div>
|
||||
<div class="form-panel" style="gap: 10px;">
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" id="layer-spbu" checked>
|
||||
<span>Stasiun SPBU</span>
|
||||
</label>
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" id="layer-jalan" checked>
|
||||
<span>Jalur Jalan</span>
|
||||
</label>
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" id="layer-rumah" checked>
|
||||
<span>Rumah Ibadah</span>
|
||||
</label>
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" id="layer-warga" checked>
|
||||
<span>Warga Miskin</span>
|
||||
</label>
|
||||
<label class="checkbox-group">
|
||||
<input type="checkbox" id="layer-choro" checked>
|
||||
<span>Choropleth Kepadatan</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Draw Tools -->
|
||||
<div class="menu-group">
|
||||
<div class="menu-title">Draw Tools</div>
|
||||
<button id="btn-draw-spbu" class="btn-action btn-spbu">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.242-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
||||
Tambah SPBU
|
||||
</button>
|
||||
<button id="btn-draw-jalan" class="btn-action btn-jalan">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path></svg>
|
||||
Tambah Jalan
|
||||
</button>
|
||||
<button id="btn-draw-kavling" class="btn-action btn-kavling">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path></svg>
|
||||
Tambah Kavling
|
||||
</button>
|
||||
<button id="btn-draw-rumah" class="btn-action btn-rumah">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path></svg>
|
||||
Tambah Rumah Ibadah
|
||||
</button>
|
||||
<button id="btn-draw-warga" class="btn-action btn-warga">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path></svg>
|
||||
Tambah Warga Miskin
|
||||
</button>
|
||||
<button id="btn-draw-spbu" class="btn-action btn-spbu"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.242-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg> Tambah SPBU</button>
|
||||
<button id="btn-draw-jalan" class="btn-action btn-jalan"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path></svg> Tambah Jalan</button>
|
||||
<button id="btn-draw-kavling" class="btn-action btn-kavling"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path></svg> Tambah Kavling</button>
|
||||
<button id="btn-draw-rumah" class="btn-action btn-rumah"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path></svg> Tambah Rumah Ibadah</button>
|
||||
<button id="btn-draw-warga" class="btn-action btn-warga"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path></svg> Tambah Warga Miskin</button>
|
||||
</div>
|
||||
|
||||
<!-- Choropleth stats panel -->
|
||||
<div id="choropleth-panel" class="form-panel" style="margin-top:4px; background:rgba(16,185,129,0.07); border-color:rgba(16,185,129,0.25);">
|
||||
<h3 style="margin:0 0 8px 0; font-size:0.85rem; color:#10B981; font-weight:600;">
|
||||
<i class="fas fa-chart-area" style="margin-right:6px;"></i>Statistik Kavling (Choropleth)
|
||||
</h3>
|
||||
<p style="font-size:0.78rem; margin:0 0 10px 0; color:var(--text-muted);">Pewarnaan otomatis area kavling berdasarkan jumlah warga miskin yang bermukim di atasnya.</p>
|
||||
<div class="stat-row" style="margin-bottom:6px;">
|
||||
<span>Total Penduduk Miskin:</span>
|
||||
<strong id="stat-total-warga" style="color:var(--text-main);">0</strong>
|
||||
<div id="choropleth-panel" class="form-panel" style="margin-top: 5px; background: rgba(16, 185, 129, 0.1); border-color: rgba(16, 185, 129, 0.3);">
|
||||
<h3 style="margin:0 0 5px 0; font-size:0.9rem; color:#10B981;">Statistik Kavling (Choropleth)</h3>
|
||||
<p style="font-size:0.8rem; margin:0 0 10px 0; color:var(--text-muted);">Pewarnaan otomatis area kavling berdasarkan jumlah warga miskin yang bermukim di atasnya.</p>
|
||||
<div style="display:flex; justify-content:space-between; margin-bottom:8px;">
|
||||
<span style="font-size:0.8rem;">Total Penduduk Miskin:</span>
|
||||
<strong id="stat-total-warga" style="color:white;">0</strong>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span>Kavling Padat (>2 warga):</span>
|
||||
<div style="display:flex; justify-content:space-between;">
|
||||
<span style="font-size:0.8rem;">Kavling Padat (>2 warga):</span>
|
||||
<strong id="stat-kavling-merah" style="color:#EF4444;">0</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic form container -->
|
||||
<div id="form-container" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<div id="toast-container" class="toast-container"></div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
|
||||
<script>
|
||||
// ── Theme toggle ──
|
||||
const html = document.documentElement;
|
||||
const btn = document.getElementById('themeToggle');
|
||||
const icon = document.getElementById('themeIcon');
|
||||
const saved = localStorage.getItem('portal-theme') || 'dark';
|
||||
setTheme(saved);
|
||||
btn.addEventListener('click', () => {
|
||||
const next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
||||
setTheme(next);
|
||||
localStorage.setItem('portal-theme', next);
|
||||
});
|
||||
function setTheme(t) {
|
||||
html.setAttribute('data-theme', t);
|
||||
icon.className = t === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
|
||||
}
|
||||
</script>
|
||||
<script type="module" src="js/app.js?v=20260613-9"></script>
|
||||
|
||||
<script type="module" src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+14
-49
@@ -30,6 +30,14 @@ window.showToast = (msg, type='success') => {
|
||||
setTimeout(() => { t.style.opacity=0; setTimeout(()=>t.remove(),300); }, 3000);
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
appMap = initMap('map');
|
||||
document.getElementById('btn-draw-rumah')?.addEventListener('click', () => { window.currentDrawType='rumah_ibadah'; new L.Draw.Marker(appMap).enable(); });
|
||||
document.getElementById('btn-draw-warga')?.addEventListener('click', () => { window.currentDrawType='warga_miskin'; new L.Draw.Marker(appMap).enable(); });
|
||||
drawControl = setupDrawControls(appMap, handleGeometryCreated);
|
||||
await loadAllData();
|
||||
});
|
||||
|
||||
const loadAllData = async () => {
|
||||
try {
|
||||
const [spbu, jalan, choro, rumah, warga] = await Promise.all([
|
||||
@@ -37,13 +45,13 @@ const loadAllData = async () => {
|
||||
rumahIbadahService.getAll(), wargaMiskinService.getAll()
|
||||
]);
|
||||
|
||||
const spbuLayer = L.geoJSON(spbu, { pointToLayer: (f, ll) => L.marker(ll, { icon: iconSPBU(f.properties.buka_24_jam) }), onEachFeature: (f, l) => bindPopup(l, 'spbu', f.properties) }).addTo(appMap);
|
||||
const jalanLayer = L.geoJSON(jalan, { style: { color: '#F59E0B', weight: 4 }, onEachFeature: (f, l) => bindPopup(l, 'jalan', f.properties) }).addTo(appMap);
|
||||
const rumahLayer = L.geoJSON(rumah, { pointToLayer: (f, ll) => L.marker(ll, { icon: iconRumah }), onEachFeature: (f, l) => bindPopup(l, 'rumah_ibadah', f.properties) }).addTo(appMap);
|
||||
const wargaLayer = L.geoJSON(warga, { pointToLayer: (f, ll) => L.marker(ll, { icon: iconWarga }), onEachFeature: (f, l) => bindPopup(l, 'warga_miskin', f.properties) }).addTo(appMap);
|
||||
L.geoJSON(spbu, { pointToLayer: (f, ll) => L.marker(ll, { icon: iconSPBU(f.properties.buka_24_jam) }), onEachFeature: (f, l) => bindPopup(l, 'spbu', f.properties) }).addTo(appMap);
|
||||
L.geoJSON(jalan, { style: { color: '#F59E0B', weight: 4 }, onEachFeature: (f, l) => bindPopup(l, 'jalan', f.properties) }).addTo(appMap);
|
||||
L.geoJSON(rumah, { pointToLayer: (f, ll) => L.marker(ll, { icon: iconRumah }), onEachFeature: (f, l) => bindPopup(l, 'rumah_ibadah', f.properties) }).addTo(appMap);
|
||||
L.geoJSON(warga, { pointToLayer: (f, ll) => L.marker(ll, { icon: iconWarga }), onEachFeature: (f, l) => bindPopup(l, 'warga_miskin', f.properties) }).addTo(appMap);
|
||||
|
||||
let merahCount = 0;
|
||||
const choroLayer = L.geoJSON(choro, {
|
||||
L.geoJSON(choro, {
|
||||
style: (f) => {
|
||||
const count = f.properties.jumlah_warga_miskin;
|
||||
let c = '#10B981'; // Hijau (0)
|
||||
@@ -54,28 +62,6 @@ const loadAllData = async () => {
|
||||
onEachFeature: (f, l) => bindPopup(l, 'kavling', f.properties)
|
||||
}).addTo(appMap);
|
||||
|
||||
// Bind checkbox events
|
||||
document.getElementById('layer-spbu').addEventListener('change', (e) => {
|
||||
if (e.target.checked) appMap.addLayer(spbuLayer);
|
||||
else appMap.removeLayer(spbuLayer);
|
||||
});
|
||||
document.getElementById('layer-jalan').addEventListener('change', (e) => {
|
||||
if (e.target.checked) appMap.addLayer(jalanLayer);
|
||||
else appMap.removeLayer(jalanLayer);
|
||||
});
|
||||
document.getElementById('layer-rumah').addEventListener('change', (e) => {
|
||||
if (e.target.checked) appMap.addLayer(rumahLayer);
|
||||
else appMap.removeLayer(rumahLayer);
|
||||
});
|
||||
document.getElementById('layer-warga').addEventListener('change', (e) => {
|
||||
if (e.target.checked) appMap.addLayer(wargaLayer);
|
||||
else appMap.removeLayer(wargaLayer);
|
||||
});
|
||||
document.getElementById('layer-choro').addEventListener('change', (e) => {
|
||||
if (e.target.checked) appMap.addLayer(choroLayer);
|
||||
else appMap.removeLayer(choroLayer);
|
||||
});
|
||||
|
||||
document.getElementById('stat-total-warga').innerText = warga.features.length;
|
||||
document.getElementById('stat-kavling-merah').innerText = merahCount;
|
||||
|
||||
@@ -115,31 +101,10 @@ const handleGeometryCreated = (type, geometry, layer) => {
|
||||
try {
|
||||
if(type==='spbu') await spbuService.create(payload);
|
||||
if(type==='jalan') await jalanService.create(payload);
|
||||
if(type==='kavling') {
|
||||
const kavlingPayload = {
|
||||
nama_pemilik: payload.nama,
|
||||
luas: payload.luas || 0,
|
||||
geometry: payload.geometry
|
||||
};
|
||||
await kavlingService.create(kavlingPayload);
|
||||
}
|
||||
if(type==='kavling') await kavlingService.create(payload);
|
||||
if(type==='rumah_ibadah') await rumahIbadahService.create(payload);
|
||||
if(type==='warga_miskin') await wargaMiskinService.create(payload);
|
||||
window.showToast('Tersimpan'); setTimeout(()=>location.reload(), 800);
|
||||
} catch(e) { window.showToast('Gagal simpan', 'error'); }
|
||||
}, () => appMap.removeLayer(tempLayer));
|
||||
};
|
||||
|
||||
const init = async () => {
|
||||
appMap = initMap('map');
|
||||
document.getElementById('btn-draw-rumah')?.addEventListener('click', () => { window.currentDrawType='rumah_ibadah'; new L.Draw.Marker(appMap).enable(); });
|
||||
document.getElementById('btn-draw-warga')?.addEventListener('click', () => { window.currentDrawType='warga_miskin'; new L.Draw.Marker(appMap).enable(); });
|
||||
drawControl = setupDrawControls(appMap, handleGeometryCreated);
|
||||
await loadAllData();
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
@@ -14,27 +14,12 @@ export const initMap = (containerId) => {
|
||||
position: 'bottomright'
|
||||
}).addTo(map);
|
||||
|
||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
const tileUrl = isDark
|
||||
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
|
||||
: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png';
|
||||
|
||||
// Tile Layer Premium (CartoDB Dark Matter / Light All)
|
||||
const tileLayer = L.tileLayer(tileUrl, {
|
||||
// Tile Layer Premium (CartoDB Dark Matter untuk Glass UI)
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
|
||||
subdomains: 'abcd',
|
||||
maxZoom: 20
|
||||
}).addTo(map);
|
||||
|
||||
// Dynamic theme updater for tile layer
|
||||
const observer = new MutationObserver(() => {
|
||||
const dark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
tileLayer.setUrl(dark
|
||||
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
|
||||
: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'
|
||||
);
|
||||
});
|
||||
observer.observe(document.documentElement, { attributes: true });
|
||||
|
||||
return map;
|
||||
};
|
||||
|
||||
@@ -42,7 +42,7 @@ export const rumahIbadahService = createService('02/backend/api/rumah_ibadah.php
|
||||
export const wargaMiskinService = createService('02/backend/api/warga_miskin.php');
|
||||
export const haversineService = {
|
||||
getDalamRadius: async (id, radius) => {
|
||||
const res = await fetch(`${BASE_URL}/02/backend/api/rumah_ibadah.php?action=jangkauan&id=${id}&radius=${radius}`);
|
||||
const res = await fetch(`${BASE_URL}/02/backend/api/haversine.php?rumah_ibadah_id=${id}&radius_km=${radius}`);
|
||||
const json = await res.json();
|
||||
return json.data;
|
||||
}
|
||||
|
||||
+5
-11
@@ -1,22 +1,16 @@
|
||||
FROM php:8.2-apache
|
||||
|
||||
# Install PHP extensions & enable Apache modules
|
||||
RUN docker-php-ext-install pdo_mysql mysqli \
|
||||
&& a2enmod rewrite headers
|
||||
|
||||
# Copy application files
|
||||
COPY portal.html /var/www/html/index.html
|
||||
COPY portal.html /var/www/html/portal.html
|
||||
COPY favicon.ico /var/www/html/favicon.ico
|
||||
COPY webgis_app/ /var/www/html/webgis_app/
|
||||
COPY db_scripts/ /var/www/html/db_scripts/
|
||||
COPY index.html /var/www/html/index.html
|
||||
COPY 01/ /var/www/html/01/
|
||||
COPY 02/ /var/www/html/02/
|
||||
COPY 03/ /var/www/html/03/
|
||||
COPY final/ /var/www/html/final/
|
||||
COPY project_final/ /var/www/html/project_final/
|
||||
COPY database/ /var/www/html/database/
|
||||
|
||||
# Set proper permissions
|
||||
RUN chown -R www-data:www-data /var/www/html \
|
||||
&& find /var/www/html -type d -exec chmod 755 {} \; \
|
||||
&& find /var/www/html -type f -exec chmod 644 {} \;
|
||||
RUN chown -R www-data:www-data /var/www/html
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
@@ -1,183 +1,75 @@
|
||||
# 🗺️ WebGIS Smart City Pontianak — Portal & Aplikasi Spasial
|
||||
**Tugas Akhir / UAS — Sistem Informasi Geografis (SIG)**
|
||||
# D1041231071 — WebGIS Smart City Project
|
||||
**Naufal Zaky R | UAS WebGIS**
|
||||
|
||||
Selamat datang di repositori **WebGIS Smart City Pontianak**. Repositori ini merupakan wadah proyek akhir mata kuliah Sistem Informasi Geografis yang mengintegrasikan seluruh modul praktikum pembelajaran dari dasar hingga aplikasi final berbasis spasial.
|
||||
Aplikasi WebGIS berbasis PHP + MySQL untuk pemetaan data kemiskinan, fasilitas umum, laporan warga, dan analisis blank spot bantuan sosial di Kota Pontianak.
|
||||
|
||||
---
|
||||
## Stack Teknologi
|
||||
|
||||
## 👤 Identitas Mahasiswa
|
||||
* **Nama**: Naufal Zaky Ramadhan
|
||||
* **NIM**: D1041231071
|
||||
* **Program Studi**: Rekayasa Sistem Komputer
|
||||
* **Mata Kuliah**: Sistem Informasi Geografis (2025/2026)
|
||||
* **Status Proyek**: Produksi / Selesai Deploy (Coolify & XAMPP)
|
||||
- PHP 8.2 Apache
|
||||
- MySQL / MariaDB + PostGIS
|
||||
- Leaflet.js (peta interaktif)
|
||||
- PDO MySQL (koneksi database)
|
||||
|
||||
---
|
||||
## Cara Deploy (Coolify)
|
||||
|
||||
## 📂 Struktur Repositori
|
||||
1. Buat database MySQL/MariaDB di Coolify.
|
||||
2. Import `db_scripts/webgis_naufal_zaky.sql` ke database tersebut.
|
||||
3. Buat aplikasi baru dari repo ini.
|
||||
4. Pilih build menggunakan `Dockerfile` di root repository.
|
||||
5. Set environment variable:
|
||||
|
||||
```text
|
||||
D1041231071_NaufalZakyR_UAS_WebGISProject/
|
||||
├── portal.html # Halaman Hub utama (Directory & Navigator Portal)
|
||||
├── 01/ # Modul Pertemuan 01 - Geometri Spasial Dasar
|
||||
│ ├── backend/ # API lokal Modul 01
|
||||
│ └── frontend/ # Tampilan Leaflet & Leaflet Draw (Geometri Dasar)
|
||||
├── 02/ # Modul Pertemuan 02 - Analisis Proksimitas Jarak
|
||||
│ ├── backend/ # API lokal Modul 02
|
||||
│ └── frontend/ # Tampilan Peta Haversine & Layer Tematik
|
||||
├── 03/ # Modul Pertemuan 03 - Analisis Spasial Lanjut
|
||||
│ ├── backend/ # API lokal Modul 03
|
||||
│ └── frontend/ # Tampilan Peta Choropleth (Point-in-Polygon)
|
||||
├── webgis_app/ # APLIKASI UTAMA (UAS - Integrated Smart City WebGIS)
|
||||
│ ├── core_config/ # Konfigurasi inti (Database, Session, Auth, Env Loader)
|
||||
│ ├── endpoints/ # Handler API data spasial (GeoJSON CRUD & Analisis)
|
||||
│ ├── api/ # API Router / Wrappers
|
||||
│ ├── panel_admin/ # Dashboard & Manajemen Data Spasial Admin
|
||||
│ ├── panel_user/ # Dashboard, Laporan Warga & Peta Interaktif User
|
||||
│ └── public_assets/ # File statis (Aesthetic CSS global, JS Peta, & Ikon)
|
||||
├── db_scripts/ # Script skema SQL & Seed Data awal
|
||||
├── Dockerfile # Konfigurasi containerization Apache + PHP 8.2 untuk Deploy
|
||||
├── .env.example # Contoh konfigurasi environment variable
|
||||
└── README.md # Dokumentasi teknis proyek
|
||||
```env
|
||||
APP_BASE_PATH=
|
||||
SESSION_SECURE=true
|
||||
CORS_ALLOWED_ORIGIN=
|
||||
DB_HOST=nama-service-database
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=webgis_db
|
||||
DB_USERNAME=user_database
|
||||
DB_PASSWORD=password_database
|
||||
```
|
||||
|
||||
---
|
||||
Aplikasi final berjalan di `/webgis_app`. Gunakan `APP_BASE_PATH=/webgis_app` untuk deploy.
|
||||
|
||||
## 🌐 Fitur Utama Pada Portal (`portal.html`)
|
||||
## Akun Awal
|
||||
|
||||
Halaman `portal.html` di root repositori bertindak sebagai **pintu gerbang interaktif (Dashboard Portal Hub)** yang menghubungkan seluruh progres pengerjaan praktikum dari awal semester hingga aplikasi final UAS.
|
||||
Setelah import SQL:
|
||||
|
||||
### Karakteristik & Fitur Portal:
|
||||
* **Rich Modern UI**: Menggunakan skema desain *sleek dark mode* secara default, dengan dukungan *theme toggle* (Dark/Light) yang tersimpan secara lokal menggunakan `localStorage`.
|
||||
* **Micro-Animations**: Efek hover interaktif, kartu portal dinamis dengan glassmorphic gradient, serta ambient glow orbs yang mempercantik estetika antarmuka.
|
||||
* **Milestone Directory**:
|
||||
1. **Modul 01 (Geometri Dasar)**: Pembelajaran cara menggambar dan menyimpan tipe data spasial: Titik/Point (direpresentasikan sebagai SPBU), Garis/LineString (Jalan), dan Poligon/Polygon (Kavling Tanah) menggunakan plugin **Leaflet Draw** ke database MySQL.
|
||||
2. **Modul 02 (Haversine & Tematik)**: Pengenalan analisis jarak melengkung bumi (formula Haversine) secara real-time untuk mendeteksi sebaran Warga Miskin dalam radius bantuan Rumah Ibadah, serta visualisasi tematik sebaran sosial.
|
||||
3. **Modul 03 (Analisis Spasial - Point in Polygon)**: Implementasi algoritma spasial untuk mendeteksi titik Warga Miskin yang berada di dalam koordinat Poligon Kavling secara dinamis guna mewarnai kavling berdasarkan tingkat kepadatan warga (Choropleth Map).
|
||||
4. **Smart City WebGIS App (UAS)**: Integrasi penuh dari modul 01, 02, dan 03 ke dalam sebuah aplikasi web multi-peran dengan fitur pelaporan warga dan ulasan fasilitas kota.
|
||||
- Admin: `admin / admin123`
|
||||
- Pengguna: `pengguna / user123`
|
||||
|
||||
---
|
||||
> Ganti password akun demo setelah deploy jika aplikasi dibuka publik.
|
||||
|
||||
## 🚀 Aplikasi Utama (`webgis_app` / Proyek Final UAS)
|
||||
## Struktur Project
|
||||
|
||||
Aplikasi **Smart City WebGIS** merupakan hasil kompilasi dan penyempurnaan dari seluruh modul pembelajaran. Aplikasi ini ditujukan untuk memetakan sarana perkotaan, menganalisis daerah rentan sosial, serta membuka jalur interaksi dua arah antara masyarakat dan pemerintah daerah.
|
||||
```
|
||||
D1041231071_NaufalZakyR_UAS_WebGISProject/
|
||||
├── beranda.html ← Halaman pemilih progres
|
||||
├── webgis_app/ ← Aplikasi final (main app)
|
||||
│ ├── panel_admin/ ← Halaman & fitur admin
|
||||
│ ├── panel_user/ ← Halaman & fitur pengguna
|
||||
│ ├── endpoints/ ← API endpoint PHP
|
||||
│ ├── core_config/ ← Konfigurasi (DB, session, auth)
|
||||
│ └── public_assets/ ← CSS & JS (global, panel, map)
|
||||
├── db_scripts/ ← SQL scripts database
|
||||
│ ├── buat_tabel.sql
|
||||
│ ├── data_awal_wilayah.sql
|
||||
│ ├── data_awal_pengguna.sql
|
||||
│ └── webgis_naufal_zaky.sql
|
||||
├── version_01/ ← Progres pertemuan 1
|
||||
├── version_02/ ← Progres pertemuan 2
|
||||
├── version_03/ ← Progres pertemuan 3
|
||||
├── legacy_version/ ← Versi lama (referensi)
|
||||
├── Dockerfile ← Docker image untuk Coolify
|
||||
└── .env ← Konfigurasi environment
|
||||
```
|
||||
|
||||
### 🛠️ Stack Teknologi & Arsitektur Spasial
|
||||
* **Bahasa Pemrograman**: PHP Native (Terstruktur, kompatibel PHP 8.0 - 8.3)
|
||||
* **Peta & Visualisasi**: Leaflet.js, Leaflet.Draw, FontAwesome, & Google Fonts (Inter, Space Grotesk)
|
||||
* **Sistem Database**: MySQL / MariaDB dengan dukungan **Spatial Index** (`SPATIAL INDEX`) dan fungsi spasial OpenGIS (`ST_Contains`, `ST_Distance_Sphere`, `ST_GeomFromText`).
|
||||
* **Security & Auth**: Proteksi Session aman, enkripsi password menggunakan `bcrypt` (`password_verify`), Prepared Statements untuk mencegah SQL Injection, dan middleware batasan hak akses peran.
|
||||
## Jalankan di XAMPP Lokal
|
||||
|
||||
---
|
||||
Set konfigurasi di `.env`:
|
||||
|
||||
### 🛡️ Fitur Bagian Admin (`panel_admin/`)
|
||||
|
||||
Panel khusus administrator perkotaan untuk mengelola seluruh data geospasial secara dinamis.
|
||||
|
||||
#### 1. Dashboard Analitik Spasial
|
||||
* Menampilkan jumlah total objek spasial terdaftar (SPBU, Ruas Jalan, Rumah Ibadah, Kavling, Warga Miskin).
|
||||
* **MySQL Spatial Counters**: Menghitung secara real-time jumlah **Kawasan Rawan** (Kawasan kumuh dengan lebih dari 3 warga miskin di dalamnya) dan jumlah **Blank Spot Bansos** (Warga miskin yang belum tercover bantuan radius rumah ibadah).
|
||||
* Menampilkan daftar entitas terbaru yang baru saja didaftarkan oleh sistem atau dilaporkan oleh warga.
|
||||
|
||||
#### 2. Peta Interaktif Admin & Drawing Tools
|
||||
* Integrasi **Leaflet.Draw** custom. Admin dapat menambah data langsung dengan menggambar di peta:
|
||||
* Menitikkan **SPBU** (Data Point + status operasional 24 jam).
|
||||
* Menitikkan **Rumah Ibadah** (Data Point + radius bantuan sosial).
|
||||
* Menitikkan **Warga Miskin** (Data Point + data ekonomi pendapatan/tanggungan).
|
||||
* Menarik garis **Jalur Jalan** (Data LineString + klasifikasi jalan).
|
||||
* Menggambar area **Kavling Tanah** (Data Polygon + pemilik & luas tanah).
|
||||
* Menggambar **Kawasan Kumuh** (Data Polygon).
|
||||
* **Sinc-Data CRUD**: Klik objek apa saja di peta untuk melihat detail atribut, mengedit properti, mengubah letak koordinat, atau menghapusnya langsung dari peta.
|
||||
|
||||
#### 3. Data Tables Manager
|
||||
* Modul CRUD berbasis tabel interaktif untuk seluruh 8 entitas data.
|
||||
* Fitur pencarian instan tanpa reload, status badge dinamis, serta integrasi input data atribut spasial secara manual.
|
||||
* **Moderasi Laporan Warga**: Admin dapat melihat keluhan lokasi dari warga, melihat titik masalah di peta, dan mengubah status laporan menjadi *Diproses*, *Selesai*, atau *Ditolak*.
|
||||
|
||||
---
|
||||
|
||||
### 👥 Fitur Bagian Pengguna (`panel_user/`)
|
||||
|
||||
Panel publik yang diperuntukkan bagi warga kota Pontianak untuk memantau kota dan berpartisipasi aktif.
|
||||
|
||||
#### 1. Peta Interaktif User (Read-Only)
|
||||
* Menampilkan seluruh layer spasial perkotaan yang dapat diaktifkan/dinonaktifkan melalui menu kontrol layer di sidebar.
|
||||
* **Legenda Dinamis**: Panduan simbol warna pada peta (seperti SPBU 24 jam berwarna hijau, SPBU terbatas berwarna jingga, dsb).
|
||||
* **Analisis Spasial Publik**: Visualisasi langsung peta Choropleth kepadatan dan sebaran Blank Spot bantuan sosial di wilayah sekitar.
|
||||
|
||||
#### 2. Alat Spasial "SPBU Terdekat" (Proximity Search)
|
||||
* Pengguna dapat menekan tombol **SPBU Terdekat** pada peta.
|
||||
* Sistem akan mendeteksi titik tengah koordinat layar peta pengguna (`map.getCenter()`) dan melakukan query spasial ke backend.
|
||||
* Backend memproses pencarian menggunakan rumus **Haversine** (`ST_Distance_Sphere`) untuk mengurutkan stasiun pengisian bahan bakar terdekat lengkap dengan informasi jarak (meter/kilometer) dan durasi operasionalnya.
|
||||
|
||||
#### 3. Laporan Warga (Citizen Crowdsourcing)
|
||||
* Pengguna dapat mengirimkan pengaduan masalah infrastruktur (misalnya: Jalan Rusak, Banjir, Lampu Jalan Mati, Fasilitas Terbengkalai).
|
||||
* **Location Tagging**: Dilengkapi dengan mini-map Leaflet interaktif di dalam modal. Pengguna cukup mengklik lokasi kerusakan di peta untuk menangkap koordinat Latitude dan Longitude secara presisi.
|
||||
* Pengguna dapat melihat daftar laporan miliknya beserta status perkembangannya (Menunggu, Diproses, Selesai). Laporan yang masih berstatus "Menunggu" dapat dibatalkan/dihapus oleh pengguna.
|
||||
|
||||
#### 4. Ulasan & Rating Fasilitas Kota
|
||||
* Pengguna dapat memberikan rating (skala 1 - 5 bintang) dan ulasan tertulis pada fasilitas publik seperti SPBU dan Rumah Ibadah.
|
||||
* Ulasan ini akan tampil pada popup informasi ketika fasilitas tersebut diklik di peta utama oleh pengguna lain.
|
||||
|
||||
---
|
||||
|
||||
### 📊 Detail Implementasi Query Spasial MySQL
|
||||
|
||||
Aplikasi ini menggunakan keunggulan MySQL Spatial Geometris untuk efisiensi kalkulasi data peta:
|
||||
|
||||
1. **Deteksi Kawasan Rawan (Kawasan Kumuh Padat Penduduk Miskin)**
|
||||
Menghitung jumlah warga miskin yang posisinya berada di dalam poligon kawasan kumuh menggunakan fungsi `ST_Contains`:
|
||||
```sql
|
||||
SELECT k.id, k.nama_kawasan, COUNT(w.id) as jumlah_warga
|
||||
FROM kawasan_kumuh k
|
||||
LEFT JOIN warga_miskin w ON ST_Contains(k.geom, w.geom)
|
||||
GROUP BY k.id HAVING jumlah_warga > 3;
|
||||
```
|
||||
|
||||
2. **Deteksi Blank Spot Bantuan Sosial**
|
||||
Mendeteksi warga miskin yang lokasinya berada di luar radius pelayanan bantuan seluruh rumah ibadah di kota menggunakan `ST_Distance_Sphere`:
|
||||
```sql
|
||||
SELECT w.id, w.nama_kk
|
||||
FROM warga_miskin w
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM rumah_ibadah ri
|
||||
WHERE ST_Distance_Sphere(w.geom, ri.geom) <= ri.radius_bantuan_meter
|
||||
);
|
||||
```
|
||||
|
||||
3. **Pencarian SPBU Terdekat (Proximity Query)**
|
||||
Mencari SPBU dengan radius terdekat dari koordinat input (`:user_lon`, `:user_lat`):
|
||||
```sql
|
||||
SELECT nama, buka_24_jam,
|
||||
ST_Distance_Sphere(geom, ST_GeomFromText('POINT(user_lon user_lat)', 4326)) AS jarak_meter
|
||||
FROM spbu
|
||||
ORDER BY jarak_meter ASC LIMIT 5;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Cara Menjalankan Project
|
||||
|
||||
### 💻 A. Menjalankan di XAMPP Lokal (Windows)
|
||||
|
||||
#### 1. Persiapan Direktori Proyek
|
||||
Salin folder proyek ini ke direktori web root XAMPP Anda (biasanya `C:\xampp\htdocs\`). Pastikan nama foldernya sesuai:
|
||||
`C:\xampp\htdocs\D1041231071_NaufalZakyR_UAS_WebGISProject`
|
||||
|
||||
#### 2. Konfigurasi Database
|
||||
1. Aktifkan service **Apache** dan **MySQL** melalui XAMPP Control Panel.
|
||||
2. Buka browser dan akses `http://localhost/phpmyadmin/`.
|
||||
3. Buat database baru bernama `webgis_db`.
|
||||
4. Import file SQL database awal yang terletak di `db_scripts/webgis_naufal_zaky.sql` ke dalam database `webgis_db`.
|
||||
> 💡 **Fitur Migrasi Otomatis**: Alternatif lain, Anda cukup membuat database kosong `webgis_db`. Saat aplikasi pertama kali dijalankan, sistem koneksi PDO akan mendeteksi database kosong dan secara otomatis mengimpor tabel serta data awal tanpa perlu manual phpMyAdmin!
|
||||
|
||||
#### 3. Pengaturan Environment (.env)
|
||||
Buat file bernama `.env` di direktori utama proyek (sejajar dengan `portal.html`) dengan isi sebagai berikut:
|
||||
```env
|
||||
APP_BASE_PATH=/D1041231071_NaufalZakyR_UAS_WebGISProject/webgis_app
|
||||
SESSION_SECURE=false
|
||||
CORS_ALLOWED_ORIGIN=http://localhost
|
||||
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=webgis_db
|
||||
@@ -185,56 +77,5 @@ DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
```
|
||||
|
||||
#### 4. Mengakses Aplikasi
|
||||
* Untuk membuka halaman **Portal Proyek**:
|
||||
`http://localhost/D1041231071_NaufalZakyR_UAS_WebGISProject/portal.html`
|
||||
* Untuk langsung menuju **Aplikasi WebGIS Utama**:
|
||||
`http://localhost/D1041231071_NaufalZakyR_UAS_WebGISProject/webgis_app/`
|
||||
|
||||
---
|
||||
|
||||
### 🐳 B. Menjalankan Menggunakan Docker
|
||||
Repositori ini telah dilengkapi dengan `Dockerfile` siap pakai untuk containerization.
|
||||
|
||||
1. Buka terminal di direktori proyek dan jalankan perintah build image:
|
||||
```bash
|
||||
docker build -t webgis-smartcity .
|
||||
```
|
||||
2. Jalankan container dengan menghubungkannya ke container database Anda:
|
||||
```bash
|
||||
docker run -d -p 8080:80 --name webgis-app webgis-smartcity
|
||||
```
|
||||
3. Akses aplikasi di `http://localhost:8080/`.
|
||||
|
||||
---
|
||||
|
||||
### ☁️ C. Deploying to Coolify (Production)
|
||||
Jika Anda men-deploy proyek ini ke cloud server menggunakan platform **Coolify**:
|
||||
1. Hubungkan akun GitHub/GitLab Anda ke Coolify.
|
||||
2. Buat database MySQL / MariaDB resource di panel Coolify.
|
||||
3. Tambahkan aplikasi baru dari repositori Git ini.
|
||||
4. Set buildpack ke **Dockerfile** (Coolify akan mendeteksi `Dockerfile` di root secara otomatis).
|
||||
5. Definisikan **Environment Variables** di panel aplikasi Coolify sesuai konfigurasi database yang dibuat:
|
||||
```env
|
||||
APP_BASE_PATH=
|
||||
SESSION_SECURE=true
|
||||
DB_HOST=mysql-service-name
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=webgis_db
|
||||
DB_USERNAME=coolify_user
|
||||
DB_PASSWORD=coolify_password
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Kredensial Akun Pengujian (Demo)
|
||||
|
||||
Gunakan akun berikut untuk masuk ke sistem di halaman login (`webgis_app/login.php`):
|
||||
|
||||
| Peran (Role) | Username | Password | Deskripsi Akses |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Administrator** | `admin` | `admin123` | Akses penuh edit spasial peta, CRUD tabel, dan moderasi laporan warga. |
|
||||
| **Warga Publik** | `pengguna` | `user123` | Akses view peta interaktif, Proximity SPBU, kirim ulasan, dan input laporan warga. |
|
||||
|
||||
> [!WARNING]
|
||||
> *Demi keamanan data publik, harap segera mengubah kata sandi default atau menghapus akun demo ini melalui menu **Manajemen Pengguna** setelah sistem di-deploy ke server produksi.*
|
||||
Import `db_scripts/webgis_naufal_zaky.sql` ke phpMyAdmin, lalu buka:
|
||||
`http://localhost/D1041231071_NaufalZakyR_UAS_WebGISProject/webgis_app/login.php`
|
||||
|
||||
@@ -21,112 +21,28 @@ SET FOREIGN_KEY_CHECKS = 1;
|
||||
-- ============================================================
|
||||
INSERT INTO spbu (nama, deskripsi, buka_24_jam, geom) VALUES
|
||||
(
|
||||
'SPBU COCO 61.781.01 Ahmad Yani',
|
||||
'Jl. Jend. Ahmad Yani No.3, Pontianak (Dekat Bundaran Digulis Untan)',
|
||||
'SPBU Bundaran Digulis Untan',
|
||||
'SPBU terdekat dari kampus',
|
||||
1,
|
||||
ST_GeomFromText('POINT(109.344381 -0.056461)')
|
||||
ST_GeomFromText('POINT(109.3440 -0.0565)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.11 Imam Bonjol',
|
||||
'Jl. Imam Bonjol No.30, Pontianak Tenggara',
|
||||
'SPBU Ahmad Yani',
|
||||
'Dekat Mega Mall (24 Jam)',
|
||||
1,
|
||||
ST_GeomFromText('POINT(109.349692 -0.038148)')
|
||||
ST_GeomFromText('POINT(109.3360 -0.0450)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.06 Kota Baru',
|
||||
'Jl. Prof. Dr. M. Yamin, Kota Baru, Pontianak Selatan',
|
||||
1,
|
||||
ST_GeomFromText('POINT(109.317589 -0.054378)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.01 Adi Sucipto',
|
||||
'Jl. Adi Sucipto, Pontianak Tenggara',
|
||||
'SPBU Imam Bonjol',
|
||||
'SPBU pinggir kota',
|
||||
0,
|
||||
ST_GeomFromText('POINT(109.362241 -0.061730)')
|
||||
ST_GeomFromText('POINT(109.3410 -0.0380)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.03 KH. Ahmad Dahlan',
|
||||
'Jl. KH. Ahmad Dahlan No.12, Pontianak Kota',
|
||||
'SPBU Kota Baru',
|
||||
'Jl. Sultan Abdurrahman',
|
||||
0,
|
||||
ST_GeomFromText('POINT(109.333830 -0.033621)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.05 Tanjung Pura',
|
||||
'Jl. Tanjung Pura, Pontianak Selatan',
|
||||
1,
|
||||
ST_GeomFromText('POINT(109.336184 -0.026788)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.12 Gusti Hamzah',
|
||||
'Jl. Gusti Hamzah (Pancasila), Pontianak Kota',
|
||||
0,
|
||||
ST_GeomFromText('POINT(109.319760 -0.029415)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.14 Husein Hamzah',
|
||||
'Jl. Husein Hamzah (Pal 3), Pontianak Barat',
|
||||
0,
|
||||
ST_GeomFromText('POINT(109.294121 -0.027963)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.15 RE Martadinata',
|
||||
'Jl. RE Martadinata, Pontianak Barat',
|
||||
0,
|
||||
ST_GeomFromText('POINT(109.314227 -0.016335)')
|
||||
),
|
||||
(
|
||||
'SPBU 63.781.02 28 Oktober',
|
||||
'Jl. 28 Oktober, Pontianak Utara',
|
||||
0,
|
||||
ST_GeomFromText('POINT(109.366405 0.007621)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.782.01 Khatulistiwa',
|
||||
'Jl. Khatulistiwa, Pontianak Utara',
|
||||
1,
|
||||
ST_GeomFromText('POINT(109.325983 0.035415)')
|
||||
),
|
||||
(
|
||||
'SPBU OSO MT. Haryono',
|
||||
'Jl. Letjen MT Haryono, Pontianak Selatan',
|
||||
0,
|
||||
ST_GeomFromText('POINT(109.3367485 -0.0448924)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.18 Danau Sentarum',
|
||||
'Jl. Danau Sentarum, Pontianak Kota',
|
||||
0,
|
||||
ST_GeomFromText('POINT(109.311283 -0.046342)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.19 Serdam',
|
||||
'Jl. Sungai Raya Dalam, Pontianak Tenggara',
|
||||
0,
|
||||
ST_GeomFromText('POINT(109.358241 -0.076389)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.02 Hasanuddin',
|
||||
'Jl. Hasanuddin, Pontianak Barat',
|
||||
0,
|
||||
ST_GeomFromText('POINT(109.321873 -0.024502)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.21 Dr. Wahidin',
|
||||
'Jl. Dr. Wahidin S., Pontianak Kota',
|
||||
0,
|
||||
ST_GeomFromText('POINT(109.308210 -0.034502)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.17 Komyos Sudarso',
|
||||
'Jl. Komodor Yos Sudarso, Pontianak Barat',
|
||||
0,
|
||||
ST_GeomFromText('POINT(109.299100 -0.019500)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.19 HOS Cokroaminoto',
|
||||
'Jl. HOS Cokroaminoto, Pontianak Kota',
|
||||
1,
|
||||
ST_GeomFromText('POINT(109.337500 -0.031500)')
|
||||
ST_GeomFromText('POINT(109.3245 -0.0487)')
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
@@ -137,49 +53,25 @@ INSERT INTO rumah_ibadah (nama, agama, radius_bantuan_meter, geom) VALUES
|
||||
'Masjid Raya Mujahidin',
|
||||
'Islam',
|
||||
1000,
|
||||
ST_GeomFromText('POINT(109.3377 -0.0414)')
|
||||
ST_GeomFromText('POINT(109.3356 -0.0454)')
|
||||
),
|
||||
(
|
||||
'Masjid Jami Sultan Syarif Abdurrahman',
|
||||
'Islam',
|
||||
1000,
|
||||
ST_GeomFromText('POINT(109.3522 -0.0229)')
|
||||
),
|
||||
(
|
||||
'Gereja Katedral Santo Yosef',
|
||||
'Gereja Katedral St. Yosef',
|
||||
'Katolik',
|
||||
1000,
|
||||
ST_GeomFromText('POINT(109.3384 -0.0274)')
|
||||
),
|
||||
(
|
||||
'Gereja Kristen Immanuel (GPIB)',
|
||||
'Kristen',
|
||||
1000,
|
||||
ST_GeomFromText('POINT(109.3375 -0.0292)')
|
||||
),
|
||||
(
|
||||
'Gereja GKKB Pontianak',
|
||||
'Kristen',
|
||||
1000,
|
||||
ST_GeomFromText('POINT(109.3386 -0.0336)')
|
||||
ST_GeomFromText('POINT(109.3368 -0.0298)')
|
||||
),
|
||||
(
|
||||
'Vihara Bodhisatva Karaniya Metta',
|
||||
'Buddha',
|
||||
1000,
|
||||
ST_GeomFromText('POINT(109.3432 -0.0215)')
|
||||
ST_GeomFromText('POINT(109.3468 -0.0233)')
|
||||
),
|
||||
(
|
||||
'Maha Vihara Maitreya',
|
||||
'Buddha',
|
||||
'Masjid Jami Keraton Kadriyah',
|
||||
'Islam',
|
||||
1000,
|
||||
ST_GeomFromText('POINT(109.3649 -0.0732)')
|
||||
),
|
||||
(
|
||||
'Pura Giripati Mulawarman',
|
||||
'Hindu',
|
||||
1000,
|
||||
ST_GeomFromText('POINT(109.3802 -0.0663)')
|
||||
ST_GeomFromText('POINT(109.3523 -0.0229)')
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
@@ -187,44 +79,19 @@ INSERT INTO rumah_ibadah (nama, agama, radius_bantuan_meter, geom) VALUES
|
||||
-- ============================================================
|
||||
INSERT INTO jalan (nama, jenis_jalan, geom) VALUES
|
||||
(
|
||||
'Jalan Jenderal Ahmad Yani',
|
||||
'Jalan Ahmad Yani',
|
||||
'Arteri Primer',
|
||||
ST_GeomFromText('LINESTRING(109.3621 -0.0768, 109.3444 -0.0565, 109.3360 -0.0450, 109.3330 -0.0330)')
|
||||
ST_GeomFromText('LINESTRING(109.3448 -0.0583, 109.3360 -0.0450, 109.3310 -0.0320)')
|
||||
),
|
||||
(
|
||||
'Jalan Gajah Mada',
|
||||
'Arteri Primer',
|
||||
ST_GeomFromText('LINESTRING(109.3310 -0.0320, 109.3380 -0.0290, 109.3450 -0.0250)')
|
||||
),
|
||||
(
|
||||
'Jalan Tanjung Pura',
|
||||
'Arteri Primer',
|
||||
ST_GeomFromText('LINESTRING(109.3300 -0.0300, 109.3360 -0.0260, 109.3480 -0.0240, 109.3550 -0.0270)')
|
||||
'Kolektor',
|
||||
ST_GeomFromText('LINESTRING(109.3310 -0.0320, 109.3410 -0.0290, 109.3460 -0.0250)')
|
||||
),
|
||||
(
|
||||
'Jalan Imam Bonjol',
|
||||
'Arteri Primer',
|
||||
ST_GeomFromText('LINESTRING(109.3360 -0.0450, 109.3420 -0.0400, 109.3500 -0.0350, 109.3590 -0.0400)')
|
||||
),
|
||||
(
|
||||
'Jalan Adi Sucipto',
|
||||
'Arteri Primer',
|
||||
ST_GeomFromText('LINESTRING(109.3590 -0.0400, 109.3650 -0.0600, 109.3750 -0.0700)')
|
||||
),
|
||||
(
|
||||
'Jalan Khatulistiwa',
|
||||
'Arteri Primer',
|
||||
ST_GeomFromText('LINESTRING(109.3450 0.0050, 109.3350 0.0150, 109.3250 0.0350, 109.3150 0.0550)')
|
||||
),
|
||||
(
|
||||
'Jalan Komodor Yos Sudarso',
|
||||
'Arteri Primer',
|
||||
ST_GeomFromText('LINESTRING(109.3250 -0.0280, 109.3150 -0.0240, 109.3000 -0.0180, 109.2850 -0.0150)')
|
||||
),
|
||||
(
|
||||
'Jalan Prof. M. Yamin',
|
||||
'Arteri Primer',
|
||||
ST_GeomFromText('LINESTRING(109.3245 -0.0487, 109.3170 -0.0540, 109.3050 -0.0620)')
|
||||
'Lokal',
|
||||
ST_GeomFromText('LINESTRING(109.3360 -0.0450, 109.3420 -0.0400, 109.3500 -0.0350)')
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
|
||||
@@ -61,112 +61,28 @@ CREATE TABLE spbu (
|
||||
|
||||
INSERT INTO spbu (nama, deskripsi, buka_24_jam, geom) VALUES
|
||||
(
|
||||
'SPBU COCO 61.781.01 Ahmad Yani',
|
||||
'Jl. Jend. Ahmad Yani No.3, Pontianak (Dekat Bundaran Digulis Untan)',
|
||||
'SPBU Bundaran Digulis Untan',
|
||||
'SPBU terdekat dari kampus',
|
||||
1,
|
||||
ST_GeomFromText('POINT(109.344381 -0.056461)')
|
||||
ST_GeomFromText('POINT(109.3440 -0.0565)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.11 Imam Bonjol',
|
||||
'Jl. Imam Bonjol No.30, Pontianak Tenggara',
|
||||
'SPBU Ahmad Yani',
|
||||
'Dekat Mega Mall (24 Jam)',
|
||||
1,
|
||||
ST_GeomFromText('POINT(109.349692 -0.038148)')
|
||||
ST_GeomFromText('POINT(109.3360 -0.0450)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.06 Kota Baru',
|
||||
'Jl. Prof. Dr. M. Yamin, Kota Baru, Pontianak Selatan',
|
||||
1,
|
||||
ST_GeomFromText('POINT(109.317589 -0.054378)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.01 Adi Sucipto',
|
||||
'Jl. Adi Sucipto, Pontianak Tenggara',
|
||||
'SPBU Imam Bonjol',
|
||||
'SPBU pinggir kota',
|
||||
0,
|
||||
ST_GeomFromText('POINT(109.362241 -0.061730)')
|
||||
ST_GeomFromText('POINT(109.3410 -0.0380)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.03 KH. Ahmad Dahlan',
|
||||
'Jl. KH. Ahmad Dahlan No.12, Pontianak Kota',
|
||||
'SPBU Kota Baru',
|
||||
'Jl. Sultan Abdurrahman',
|
||||
0,
|
||||
ST_GeomFromText('POINT(109.333830 -0.033621)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.05 Tanjung Pura',
|
||||
'Jl. Tanjung Pura, Pontianak Selatan',
|
||||
1,
|
||||
ST_GeomFromText('POINT(109.336184 -0.026788)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.12 Gusti Hamzah',
|
||||
'Jl. Gusti Hamzah (Pancasila), Pontianak Kota',
|
||||
0,
|
||||
ST_GeomFromText('POINT(109.319760 -0.029415)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.14 Husein Hamzah',
|
||||
'Jl. Husein Hamzah (Pal 3), Pontianak Barat',
|
||||
0,
|
||||
ST_GeomFromText('POINT(109.294121 -0.027963)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.15 RE Martadinata',
|
||||
'Jl. RE Martadinata, Pontianak Barat',
|
||||
0,
|
||||
ST_GeomFromText('POINT(109.314227 -0.016335)')
|
||||
),
|
||||
(
|
||||
'SPBU 63.781.02 28 Oktober',
|
||||
'Jl. 28 Oktober, Pontianak Utara',
|
||||
0,
|
||||
ST_GeomFromText('POINT(109.366405 0.007621)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.782.01 Khatulistiwa',
|
||||
'Jl. Khatulistiwa, Pontianak Utara',
|
||||
1,
|
||||
ST_GeomFromText('POINT(109.325983 0.035415)')
|
||||
),
|
||||
(
|
||||
'SPBU OSO MT. Haryono',
|
||||
'Jl. Letjen MT Haryono, Pontianak Selatan',
|
||||
0,
|
||||
ST_GeomFromText('POINT(109.3367485 -0.0448924)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.18 Danau Sentarum',
|
||||
'Jl. Danau Sentarum, Pontianak Kota',
|
||||
0,
|
||||
ST_GeomFromText('POINT(109.311283 -0.046342)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.19 Serdam',
|
||||
'Jl. Sungai Raya Dalam, Pontianak Tenggara',
|
||||
0,
|
||||
ST_GeomFromText('POINT(109.358241 -0.076389)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.02 Hasanuddin',
|
||||
'Jl. Hasanuddin, Pontianak Barat',
|
||||
0,
|
||||
ST_GeomFromText('POINT(109.321873 -0.024502)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.21 Dr. Wahidin',
|
||||
'Jl. Dr. Wahidin S., Pontianak Kota',
|
||||
0,
|
||||
ST_GeomFromText('POINT(109.308210 -0.034502)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.17 Komyos Sudarso',
|
||||
'Jl. Komodor Yos Sudarso, Pontianak Barat',
|
||||
0,
|
||||
ST_GeomFromText('POINT(109.299100 -0.019500)')
|
||||
),
|
||||
(
|
||||
'SPBU 64.781.19 HOS Cokroaminoto',
|
||||
'Jl. HOS Cokroaminoto, Pontianak Kota',
|
||||
1,
|
||||
ST_GeomFromText('POINT(109.337500 -0.031500)')
|
||||
ST_GeomFromText('POINT(109.3245 -0.0487)')
|
||||
);
|
||||
|
||||
CREATE TABLE rumah_ibadah (
|
||||
@@ -184,49 +100,25 @@ INSERT INTO rumah_ibadah (nama, agama, radius_bantuan_meter, geom) VALUES
|
||||
'Masjid Raya Mujahidin',
|
||||
'Islam',
|
||||
1000,
|
||||
ST_GeomFromText('POINT(109.3377 -0.0414)')
|
||||
ST_GeomFromText('POINT(109.3356 -0.0454)')
|
||||
),
|
||||
(
|
||||
'Masjid Jami Sultan Syarif Abdurrahman',
|
||||
'Islam',
|
||||
1000,
|
||||
ST_GeomFromText('POINT(109.3522 -0.0229)')
|
||||
),
|
||||
(
|
||||
'Gereja Katedral Santo Yosef',
|
||||
'Gereja Katedral St. Yosef',
|
||||
'Katolik',
|
||||
1000,
|
||||
ST_GeomFromText('POINT(109.3384 -0.0274)')
|
||||
),
|
||||
(
|
||||
'Gereja Kristen Immanuel (GPIB)',
|
||||
'Kristen',
|
||||
1000,
|
||||
ST_GeomFromText('POINT(109.3375 -0.0292)')
|
||||
),
|
||||
(
|
||||
'Gereja GKKB Pontianak',
|
||||
'Kristen',
|
||||
1000,
|
||||
ST_GeomFromText('POINT(109.3386 -0.0336)')
|
||||
ST_GeomFromText('POINT(109.3368 -0.0298)')
|
||||
),
|
||||
(
|
||||
'Vihara Bodhisatva Karaniya Metta',
|
||||
'Buddha',
|
||||
1000,
|
||||
ST_GeomFromText('POINT(109.3432 -0.0215)')
|
||||
ST_GeomFromText('POINT(109.3468 -0.0233)')
|
||||
),
|
||||
(
|
||||
'Maha Vihara Maitreya',
|
||||
'Buddha',
|
||||
'Masjid Jami Keraton Kadriyah',
|
||||
'Islam',
|
||||
1000,
|
||||
ST_GeomFromText('POINT(109.3649 -0.0732)')
|
||||
),
|
||||
(
|
||||
'Pura Giripati Mulawarman',
|
||||
'Hindu',
|
||||
1000,
|
||||
ST_GeomFromText('POINT(109.3802 -0.0663)')
|
||||
ST_GeomFromText('POINT(109.3523 -0.0229)')
|
||||
);
|
||||
|
||||
CREATE TABLE jalan (
|
||||
@@ -240,44 +132,19 @@ CREATE TABLE jalan (
|
||||
|
||||
INSERT INTO jalan (nama, jenis_jalan, geom) VALUES
|
||||
(
|
||||
'Jalan Jenderal Ahmad Yani',
|
||||
'Jalan Ahmad Yani',
|
||||
'Arteri Primer',
|
||||
ST_GeomFromText('LINESTRING(109.3621 -0.0768, 109.3444 -0.0565, 109.3360 -0.0450, 109.3330 -0.0330)')
|
||||
ST_GeomFromText('LINESTRING(109.3448 -0.0583, 109.3360 -0.0450, 109.3310 -0.0320)')
|
||||
),
|
||||
(
|
||||
'Jalan Gajah Mada',
|
||||
'Arteri Primer',
|
||||
ST_GeomFromText('LINESTRING(109.3310 -0.0320, 109.3380 -0.0290, 109.3450 -0.0250)')
|
||||
),
|
||||
(
|
||||
'Jalan Tanjung Pura',
|
||||
'Arteri Primer',
|
||||
ST_GeomFromText('LINESTRING(109.3300 -0.0300, 109.3360 -0.0260, 109.3480 -0.0240, 109.3550 -0.0270)')
|
||||
'Kolektor',
|
||||
ST_GeomFromText('LINESTRING(109.3310 -0.0320, 109.3410 -0.0290, 109.3460 -0.0250)')
|
||||
),
|
||||
(
|
||||
'Jalan Imam Bonjol',
|
||||
'Arteri Primer',
|
||||
ST_GeomFromText('LINESTRING(109.3360 -0.0450, 109.3420 -0.0400, 109.3500 -0.0350, 109.3590 -0.0400)')
|
||||
),
|
||||
(
|
||||
'Jalan Adi Sucipto',
|
||||
'Arteri Primer',
|
||||
ST_GeomFromText('LINESTRING(109.3590 -0.0400, 109.3650 -0.0600, 109.3750 -0.0700)')
|
||||
),
|
||||
(
|
||||
'Jalan Khatulistiwa',
|
||||
'Arteri Primer',
|
||||
ST_GeomFromText('LINESTRING(109.3450 0.0050, 109.3350 0.0150, 109.3250 0.0350, 109.3150 0.0550)')
|
||||
),
|
||||
(
|
||||
'Jalan Komodor Yos Sudarso',
|
||||
'Arteri Primer',
|
||||
ST_GeomFromText('LINESTRING(109.3250 -0.0280, 109.3150 -0.0240, 109.3000 -0.0180, 109.2850 -0.0150)')
|
||||
),
|
||||
(
|
||||
'Jalan Prof. M. Yamin',
|
||||
'Arteri Primer',
|
||||
ST_GeomFromText('LINESTRING(109.3245 -0.0487, 109.3170 -0.0540, 109.3050 -0.0620)')
|
||||
'Lokal',
|
||||
ST_GeomFromText('LINESTRING(109.3360 -0.0450, 109.3420 -0.0400, 109.3500 -0.0350)')
|
||||
);
|
||||
|
||||
CREATE TABLE kavling (
|
||||
|
||||
+320
@@ -0,0 +1,320 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Portal WebGIS - Sistem Informasi Geografis</title>
|
||||
<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://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||
<style>
|
||||
:root {
|
||||
/* Matching webgis_app/main.css */
|
||||
--primary: #2563EB;
|
||||
--primary-light: #3B82F6;
|
||||
--primary-dark: #1D4ED8;
|
||||
--accent: #10B981;
|
||||
--bg-base: #F9FAFB;
|
||||
--bg-surface: #FFFFFF;
|
||||
--border-light: #E5E7EB;
|
||||
--border-hover: #D1D5DB;
|
||||
--text-primary: #111827;
|
||||
--text-secondary: #4B5563;
|
||||
--text-muted: #6B7280;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-xl: 16px;
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
--transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: var(--bg-base);
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
width: 100%;
|
||||
background: var(--bg-surface);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
padding: 16px 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-top: 60px;
|
||||
margin-bottom: 50px;
|
||||
animation: slideDown 0.4s ease-out;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
font-size: 3rem;
|
||||
color: var(--primary);
|
||||
margin-bottom: 16px;
|
||||
background: #EFF6FF;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
line-height: 80px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 1.05rem;
|
||||
color: var(--text-secondary);
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 24px;
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
padding: 0 24px 60px 24px;
|
||||
animation: fadeIn 0.6s ease-out forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 30px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-md);
|
||||
border-color: var(--border-hover);
|
||||
}
|
||||
|
||||
.card-icon-wrap {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--radius-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 20px;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.card:nth-child(1) .card-icon-wrap { background: #EFF6FF; color: var(--primary); }
|
||||
.card:nth-child(2) .card-icon-wrap { background: #ECFDF5; color: var(--accent); }
|
||||
.card:nth-child(3) .card-icon-wrap { background: #FEF2F2; color: #EF4444; }
|
||||
|
||||
.card-number {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
font-size: 0.9rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.card-footer i {
|
||||
margin-left: 8px;
|
||||
font-size: 0.8rem;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.card:hover .card-footer {
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.card:hover .card-footer i {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
/* Final Card styling */
|
||||
.card.final {
|
||||
background: var(--primary);
|
||||
border: 1px solid var(--primary-dark);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card.final .card-icon-wrap {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card.final .card-number {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.card.final .card-title {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card.final .card-desc {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.card.final .card-footer {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card.final:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from { opacity: 0; transform: translateY(-20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: scale(0.98); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar">
|
||||
<div class="navbar-brand">
|
||||
<i class="fas fa-map-marked-alt"></i>
|
||||
WebGIS Portal
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<header class="header">
|
||||
<div class="header-icon">
|
||||
<i class="fas fa-globe-asia"></i>
|
||||
</div>
|
||||
<h1>Sistem Informasi Geografis</h1>
|
||||
<p>Direktori tugas dan proyek akhir mata kuliah Sistem Informasi Geografis. Pilih modul pembelajaran di bawah ini untuk memulai.</p>
|
||||
</header>
|
||||
|
||||
<div class="grid-container">
|
||||
|
||||
<!-- Pertemuan 01 -->
|
||||
<a href="01/frontend/" class="card" style="animation-delay: 0.1s;">
|
||||
<div class="card-icon-wrap">
|
||||
<i class="fas fa-draw-polygon"></i>
|
||||
</div>
|
||||
<div class="card-number">Pertemuan 01</div>
|
||||
<h2 class="card-title">Geometri Dasar</h2>
|
||||
<p class="card-desc">Fitur menggambar peta dasar dan CRUD spasial.</p>
|
||||
<div class="card-footer">
|
||||
Buka Modul <i class="fas fa-arrow-right"></i>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Pertemuan 02 -->
|
||||
<a href="02/frontend/" class="card" style="animation-delay: 0.15s;">
|
||||
<div class="card-icon-wrap">
|
||||
<i class="fas fa-route"></i>
|
||||
</div>
|
||||
<div class="card-number">Pertemuan 02</div>
|
||||
<h2 class="card-title">Haversine & Tematik</h2>
|
||||
<p class="card-desc">Pemetaan sosial dan query pencarian radius jarak.</p>
|
||||
<div class="card-footer">
|
||||
Buka Modul <i class="fas fa-arrow-right"></i>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Pertemuan 03 -->
|
||||
<a href="03/frontend/" class="card" style="animation-delay: 0.2s;">
|
||||
<div class="card-icon-wrap">
|
||||
<i class="fas fa-layer-group"></i>
|
||||
</div>
|
||||
<div class="card-number">Pertemuan 03</div>
|
||||
<h2 class="card-title">Analisis Spasial</h2>
|
||||
<p class="card-desc">Visualisasi Peta Choropleth dinamis (Point-in-Polygon).</p>
|
||||
<div class="card-footer">
|
||||
Buka Modul <i class="fas fa-arrow-right"></i>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Final (UAS) -->
|
||||
<a href="webgis_app/" class="card final" style="animation-delay: 0.25s;">
|
||||
<div class="card-icon-wrap">
|
||||
<i class="fas fa-laptop-code"></i>
|
||||
</div>
|
||||
<div class="card-number">Proyek Akhir</div>
|
||||
<h2 class="card-title">Final UAS</h2>
|
||||
<p class="card-desc">Sistem komprehensif untuk evaluasi akhir semester.</p>
|
||||
<div class="card-footer">
|
||||
Buka Aplikasi <i class="fas fa-arrow-right"></i>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
-846
@@ -1,846 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id" data-theme="dark">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebGIS Portal — Sistem Informasi Geografis</title>
|
||||
<meta name="description"
|
||||
content="Portal WebGIS — Direktori modul dan proyek akhir mata kuliah Sistem Informasi Geografis.">
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Space+Grotesk:wght@400;500;600;700&display=swap"
|
||||
rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||
<style>
|
||||
/* ══ DESIGN TOKENS ══ */
|
||||
:root {
|
||||
--font-sans: 'Inter', sans-serif;
|
||||
--font-display: 'Space Grotesk', sans-serif;
|
||||
--ease: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition: all 0.25s var(--ease);
|
||||
}
|
||||
|
||||
/* ══ DARK THEME (default) ══ */
|
||||
[data-theme="dark"] {
|
||||
--bg-page: #0A0F1E;
|
||||
--bg-surface: #111827;
|
||||
--bg-card: #141D2F;
|
||||
--bg-card-hover: #1A2540;
|
||||
--border: rgba(255, 255, 255, 0.07);
|
||||
--border-hover: rgba(99, 102, 241, 0.5);
|
||||
--text-primary: #F1F5F9;
|
||||
--text-secondary: #94A3B8;
|
||||
--text-muted: #64748B;
|
||||
--primary: #6366F1;
|
||||
--primary-glow: rgba(99, 102, 241, 0.25);
|
||||
--accent: #06B6D4;
|
||||
--accent-2: #10B981;
|
||||
--accent-3: #F43F5E;
|
||||
--nav-bg: rgba(10, 15, 30, 0.85);
|
||||
--glass: rgba(17, 24, 39, 0.8);
|
||||
--shine: rgba(255, 255, 255, 0.03);
|
||||
--shadow-card: 0 4px 24px rgba(0, 0, 0, 0.4);
|
||||
--shadow-glow: 0 0 40px rgba(99, 102, 241, 0.15);
|
||||
--gradient-hero: linear-gradient(135deg, #0A0F1E 0%, #111827 50%, #0D1B2E 100%);
|
||||
--dot-color: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
/* ══ LIGHT THEME ══ */
|
||||
[data-theme="light"] {
|
||||
--bg-page: #F0F4FF;
|
||||
--bg-surface: #FFFFFF;
|
||||
--bg-card: #FFFFFF;
|
||||
--bg-card-hover: #F8FAFF;
|
||||
--border: #E2E8F0;
|
||||
--border-hover: #6366F1;
|
||||
--text-primary: #0F172A;
|
||||
--text-secondary: #475569;
|
||||
--text-muted: #94A3B8;
|
||||
--primary: #4F46E5;
|
||||
--primary-glow: rgba(79, 70, 229, 0.12);
|
||||
--accent: #0891B2;
|
||||
--accent-2: #059669;
|
||||
--accent-3: #E11D48;
|
||||
--nav-bg: rgba(240, 244, 255, 0.9);
|
||||
--glass: rgba(255, 255, 255, 0.9);
|
||||
--shine: rgba(255, 255, 255, 0.5);
|
||||
--shadow-card: 0 4px 24px rgba(79, 70, 229, 0.08);
|
||||
--shadow-glow: 0 0 40px rgba(79, 70, 229, 0.1);
|
||||
--gradient-hero: linear-gradient(135deg, #EEF2FF 0%, #F0F4FF 50%, #E8F4FF 100%);
|
||||
--dot-color: rgba(79, 70, 229, 0.04);
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--bg-page);
|
||||
color: var(--text-primary);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
transition: background 0.3s var(--ease), color 0.3s var(--ease);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* ── Dot grid background ── */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-image: radial-gradient(circle, var(--dot-color) 1px, transparent 1px);
|
||||
background-size: 32px 32px;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* ── Ambient glow orbs ── */
|
||||
.orb {
|
||||
position: fixed;
|
||||
border-radius: 50%;
|
||||
filter: blur(120px);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
transition: opacity 0.4s;
|
||||
}
|
||||
|
||||
.orb-1 {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: var(--primary-glow);
|
||||
top: -100px;
|
||||
left: -100px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.orb-2 {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background: rgba(6, 182, 212, 0.1);
|
||||
bottom: -80px;
|
||||
right: -80px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* ── Navbar ── */
|
||||
.navbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
width: 100%;
|
||||
background: var(--nav-bg);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 0 32px;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: linear-gradient(135deg, var(--primary), var(--accent));
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.95rem;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.navbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.nav-pill {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px;
|
||||
padding: 4px 14px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
/* ── Theme toggle ── */
|
||||
.theme-toggle {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-card);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: white;
|
||||
transform: rotate(15deg);
|
||||
}
|
||||
|
||||
/* ── Hero section ── */
|
||||
.hero {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
padding: 80px 24px 60px;
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 16px;
|
||||
border-radius: 100px;
|
||||
background: var(--primary-glow);
|
||||
border: 1px solid var(--border-hover);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 24px;
|
||||
animation: fadeSlideDown 0.5s var(--ease) both;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(2.2rem, 5vw, 3.8rem);
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.03em;
|
||||
margin-bottom: 20px;
|
||||
animation: fadeSlideDown 0.5s var(--ease) 0.05s both;
|
||||
}
|
||||
|
||||
.hero h1 .gradient-text {
|
||||
background: linear-gradient(135deg, var(--primary), var(--accent));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-secondary);
|
||||
max-width: 560px;
|
||||
margin: 0 auto 48px;
|
||||
line-height: 1.7;
|
||||
animation: fadeSlideDown 0.5s var(--ease) 0.1s both;
|
||||
}
|
||||
|
||||
/* ── Stats bar ── */
|
||||
.stats-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 32px;
|
||||
margin-bottom: 64px;
|
||||
animation: fadeSlideDown 0.5s var(--ease) 0.15s both;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.stat-divider {
|
||||
width: 1px;
|
||||
height: 40px;
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
/* ── Grid ── */
|
||||
.grid-section {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px 80px;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.section-label::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* ── Card ── */
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px;
|
||||
padding: 28px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: var(--shadow-card);
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--shine);
|
||||
border-radius: inherit;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-6px);
|
||||
border-color: var(--border-hover);
|
||||
box-shadow: var(--shadow-glow), var(--shadow-card);
|
||||
background: var(--bg-card-hover);
|
||||
}
|
||||
|
||||
.card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Number badge */
|
||||
.card-num {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
font-family: var(--font-display);
|
||||
font-size: 4rem;
|
||||
font-weight: 900;
|
||||
color: var(--border);
|
||||
line-height: 1;
|
||||
letter-spacing: -0.05em;
|
||||
transition: var(--transition);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.card:hover .card-num {
|
||||
color: var(--primary-glow);
|
||||
}
|
||||
|
||||
.card-icon-wrap {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.4rem;
|
||||
margin-bottom: 24px;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.card:hover .card-icon-wrap {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
/* Card themes */
|
||||
.card[data-type="p01"] .card-icon-wrap {
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
color: #818CF8;
|
||||
}
|
||||
|
||||
.card[data-type="p02"] .card-icon-wrap {
|
||||
background: rgba(6, 182, 212, 0.15);
|
||||
color: #22D3EE;
|
||||
}
|
||||
|
||||
.card[data-type="p03"] .card-icon-wrap {
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
color: #34D399;
|
||||
}
|
||||
|
||||
.card[data-type="uas"] .card-icon-wrap {
|
||||
background: rgba(244, 63, 94, 0.15);
|
||||
color: #FB7185;
|
||||
}
|
||||
|
||||
.card-tag {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 10px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.card-footer .arrow {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 6px;
|
||||
background: var(--primary-glow);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.7rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.card:hover .card-footer {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.card:hover .card-footer .arrow {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
/* ── Final (UAS) card — full-width special ── */
|
||||
.card-uas {
|
||||
grid-column: 1 / -1;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
background: linear-gradient(135deg, var(--primary) 0%, #4338CA 100%);
|
||||
border-color: transparent;
|
||||
padding: 36px 40px;
|
||||
}
|
||||
|
||||
.card-uas::before {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.card-uas:hover {
|
||||
background: var(--bg-card-hover);
|
||||
border-color: var(--border-hover);
|
||||
}
|
||||
|
||||
.card-uas .card-icon-wrap {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: white;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
font-size: 1.8rem;
|
||||
flex-shrink: 0;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.card-uas:hover .card-icon-wrap {
|
||||
background: rgba(244, 63, 94, 0.15);
|
||||
color: #FB7185;
|
||||
}
|
||||
|
||||
.card-uas .card-tag {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.card-uas:hover .card-tag {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.card-uas .card-title {
|
||||
color: white;
|
||||
font-size: 1.6rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.card-uas:hover .card-title {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-uas .card-desc {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 1rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.card-uas:hover .card-desc {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.card-uas .card-footer {
|
||||
color: white;
|
||||
margin-top: 0;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.card-uas:hover .card-footer {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.card-uas .card-num {
|
||||
color: rgba(255, 255, 255, 0.08);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.card-uas:hover .card-num {
|
||||
color: var(--primary-glow);
|
||||
}
|
||||
|
||||
.card-uas .card-footer .arrow {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.card-uas:hover .card-footer .arrow {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card-uas-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* ── Footer ── */
|
||||
.footer {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-top: auto;
|
||||
padding: 24px 32px;
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ══ ANIMATIONS ══ */
|
||||
@keyframes fadeSlideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-12px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
animation: fadeSlideUp 0.5s var(--ease) both;
|
||||
}
|
||||
|
||||
.card:nth-child(1) {
|
||||
animation-delay: 0.15s;
|
||||
}
|
||||
|
||||
.card:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.card:nth-child(3) {
|
||||
animation-delay: 0.25s;
|
||||
}
|
||||
|
||||
.card:nth-child(4) {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
@keyframes fadeSlideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ══ RESPONSIVE ══ */
|
||||
@media (max-width: 900px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.card-uas {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-uas .card-footer {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 48px 16px 40px;
|
||||
}
|
||||
|
||||
.stats-bar {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.stat-divider {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- Ambient orbs -->
|
||||
<div class="orb orb-1"></div>
|
||||
<div class="orb orb-2"></div>
|
||||
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar">
|
||||
<a href="#" class="navbar-brand">
|
||||
<div class="brand-icon"><i class="fas fa-map-marked-alt"></i></div>
|
||||
WebGIS Portal
|
||||
</a>
|
||||
<div class="navbar-right">
|
||||
<span class="nav-pill">SIG 2025/2026</span>
|
||||
<button class="theme-toggle" id="themeToggle" title="Toggle tema" aria-label="Toggle dark/light mode">
|
||||
<i class="fas fa-moon" id="themeIcon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Hero -->
|
||||
<section class="hero">
|
||||
<div class="hero-badge">
|
||||
<i class="fas fa-satellite-dish"></i>
|
||||
Sistem Informasi Geografis
|
||||
</div>
|
||||
<h1>
|
||||
Direktori Proyek<br>
|
||||
<span class="gradient-text">WebGIS Interaktif</span>
|
||||
</h1>
|
||||
<p>Kumpulan modul pembelajaran dan proyek akhir mata kuliah Sistem Informasi Geografis. Pilih modul untuk
|
||||
memulai eksplorasi.</p>
|
||||
|
||||
<div class="stats-bar">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">3</div>
|
||||
<div class="stat-label">Modul Praktikum</div>
|
||||
</div>
|
||||
<div class="stat-divider"></div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">1</div>
|
||||
<div class="stat-label">Proyek Akhir</div>
|
||||
</div>
|
||||
<div class="stat-divider"></div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">GIS</div>
|
||||
<div class="stat-label">Teknologi</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Grid -->
|
||||
<div class="grid-section">
|
||||
<div class="section-label">Modul Pembelajaran</div>
|
||||
|
||||
<div class="grid">
|
||||
<!-- Pertemuan 01 -->
|
||||
<a href="01/frontend/" class="card" data-type="p01">
|
||||
<span class="card-num">01</span>
|
||||
<div class="card-icon-wrap">
|
||||
<i class="fas fa-draw-polygon"></i>
|
||||
</div>
|
||||
<div class="card-tag">Pertemuan 01</div>
|
||||
<h2 class="card-title">Geometri Dasar</h2>
|
||||
<p class="card-desc">Menggambar dan mengelola geometri spasial: titik (SPBU), garis (Jalan), dan poligon
|
||||
(Kavling) dengan operasi CRUD.</p>
|
||||
<div class="card-footer">
|
||||
Buka Modul
|
||||
<div class="arrow"><i class="fas fa-arrow-right"></i></div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Pertemuan 02 -->
|
||||
<a href="02/frontend/" class="card" data-type="p02">
|
||||
<span class="card-num">02</span>
|
||||
<div class="card-icon-wrap">
|
||||
<i class="fas fa-route"></i>
|
||||
</div>
|
||||
<div class="card-tag">Pertemuan 02</div>
|
||||
<h2 class="card-title">Haversine & Tematik</h2>
|
||||
<p class="card-desc">Pemetaan sosial dengan analisis radius jarak Haversine dan visualisasi tematik data
|
||||
kemiskinan penduduk.</p>
|
||||
<div class="card-footer">
|
||||
Buka Modul
|
||||
<div class="arrow"><i class="fas fa-arrow-right"></i></div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Pertemuan 03 -->
|
||||
<a href="03/frontend/" class="card" data-type="p03">
|
||||
<span class="card-num">03</span>
|
||||
<div class="card-icon-wrap">
|
||||
<i class="fas fa-layer-group"></i>
|
||||
</div>
|
||||
<div class="card-tag">Pertemuan 03</div>
|
||||
<h2 class="card-title">Analisis Spasial</h2>
|
||||
<p class="card-desc">Choropleth Map dinamis berbasis Point-in-Polygon — pewarnaan otomatis area kavling
|
||||
oleh kepadatan warga miskin.</p>
|
||||
<div class="card-footer">
|
||||
Buka Modul
|
||||
<div class="arrow"><i class="fas fa-arrow-right"></i></div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Final UAS -->
|
||||
<a href="webgis_app/" class="card card-uas" data-type="uas">
|
||||
<span class="card-num">UAS</span>
|
||||
<div class="card-icon-wrap">
|
||||
<i class="fas fa-laptop-code"></i>
|
||||
</div>
|
||||
<div class="card-uas-text">
|
||||
<div class="card-tag">Proyek Akhir — Final UAS</div>
|
||||
<h2 class="card-title">Smart City WebGIS</h2>
|
||||
<p class="card-desc">Sistem komprehensif manajemen kawasan kota dengan dashboard analitik,
|
||||
autentikasi peran, dan visualisasi peta interaktif real-time.</p>
|
||||
<div class="card-footer" style="margin-top:20px;">
|
||||
Buka Aplikasi
|
||||
<div class="arrow"><i class="fas fa-arrow-right"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<span>D1041231071 — Naufal Zaky Ramadhan</span>
|
||||
<span>Sistem Informasi Geografis © 2025</span>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// ── Theme toggle ──
|
||||
const html = document.documentElement;
|
||||
const btn = document.getElementById('themeToggle');
|
||||
const icon = document.getElementById('themeIcon');
|
||||
|
||||
// Persist preference
|
||||
const saved = localStorage.getItem('portal-theme') || 'dark';
|
||||
setTheme(saved);
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
const next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
||||
setTheme(next);
|
||||
localStorage.setItem('portal-theme', next);
|
||||
});
|
||||
|
||||
function setTheme(t) {
|
||||
html.setAttribute('data-theme', t);
|
||||
icon.className = t === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
|
||||
btn.title = t === 'dark' ? 'Beralih ke Mode Terang' : 'Beralih ke Mode Gelap';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
/**
|
||||
* jalan.php
|
||||
* Tanggung Jawab: Menangani operasi CRUD untuk entitas Jalan (LineString).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
/**
|
||||
* kavling.php
|
||||
* Tanggung Jawab: Menangani operasi CRUD untuk entitas Kavling (Polygon).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
/**
|
||||
* rumah_ibadah.php
|
||||
* Tanggung Jawab: Operasi CRUD Rumah Ibadah + Endpoint analisis jangkauan Haversine.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
require_once '../core_config/database.php';
|
||||
require_once '../utils/response_helper.php';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
/**
|
||||
* statistik.php
|
||||
* Tanggung Jawab: Menyediakan data analisis spasial menggunakan Point in Polygon (ST_Contains).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
/**
|
||||
* warga_miskin.php
|
||||
* Tanggung Jawab: Operasi CRUD untuk Warga Miskin.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
/**
|
||||
* Singleton PDO connection.
|
||||
*
|
||||
@@ -32,43 +32,24 @@ class Database {
|
||||
$password = self::env('DB_PASSWORD', '');
|
||||
}
|
||||
|
||||
try {
|
||||
self::$conn = new PDO(
|
||||
"mysql:host={$host};port={$port};dbname={$dbName};charset=utf8mb4",
|
||||
$username,
|
||||
$password
|
||||
);
|
||||
self::$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
self::$conn->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||
self::$conn = new PDO(
|
||||
"mysql:host={$host};port={$port};dbname={$dbName};charset=utf8mb4",
|
||||
$username,
|
||||
$password
|
||||
);
|
||||
self::$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
self::$conn->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||
|
||||
// Disable strict mode for GROUP BY to prevent 1055 errors in Coolify's MariaDB
|
||||
self::$conn->exec("SET SESSION sql_mode=(SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY',''))");
|
||||
// Disable strict mode for GROUP BY to prevent 1055 errors in Coolify's MariaDB
|
||||
self::$conn->exec("SET SESSION sql_mode=(SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY',''))");
|
||||
|
||||
// Auto-initialize database if empty
|
||||
$stmt = self::$conn->query("SHOW TABLES LIKE 'users'");
|
||||
if ($stmt->rowCount() == 0) {
|
||||
$sqlFile = __DIR__ . '/../../db_scripts/webgis_naufal_zaky.sql';
|
||||
if (file_exists($sqlFile)) {
|
||||
$sql = file_get_contents($sqlFile);
|
||||
self::$conn->exec($sql);
|
||||
}
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
$isApi = (defined('JSON_REQUEST') && JSON_REQUEST) ||
|
||||
(strpos($_SERVER['REQUEST_URI'] ?? '', '/api/') !== false) ||
|
||||
(strpos($_SERVER['REQUEST_URI'] ?? '', 'endpoints/') !== false) ||
|
||||
(isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false);
|
||||
|
||||
if ($isApi) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => 'Database Connection Failed: ' . $e->getMessage()
|
||||
]);
|
||||
exit();
|
||||
} else {
|
||||
die("Database Connection Failed: " . $e->getMessage());
|
||||
// Auto-initialize database if empty
|
||||
$stmt = self::$conn->query("SHOW TABLES LIKE 'users'");
|
||||
if ($stmt->rowCount() == 0) {
|
||||
$sqlFile = __DIR__ . '/../../db_scripts/webgis_naufal_zaky.sql';
|
||||
if (file_exists($sqlFile)) {
|
||||
$sql = file_get_contents($sqlFile);
|
||||
self::$conn->exec($sql);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ try {
|
||||
'deskripsi' => $row['deskripsi'],
|
||||
'buka_24_jam' => (bool) $row['buka_24_jam'],
|
||||
'jarak_meter' => round($row['jarak_meter'], 2),
|
||||
'jarak_km' => round($row['jarak_meter'] / 1000, 2),
|
||||
'created_at' => $row['created_at'],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -27,18 +27,14 @@ if ($method !== 'GET') {
|
||||
}
|
||||
|
||||
try {
|
||||
date_default_timezone_set('Asia/Jakarta');
|
||||
$stmt = $pdo->query(
|
||||
"SELECT
|
||||
COUNT(*) AS total_spbu,
|
||||
COALESCE(SUM(buka_24_jam = 1), 0) AS spbu_buka,
|
||||
COALESCE(SUM(buka_24_jam = 0), 0) AS spbu_tutup
|
||||
COUNT(*) AS total,
|
||||
SUM(buka_24_jam = 1) AS buka_24_jam,
|
||||
SUM(buka_24_jam = 0) AS tidak_24_jam
|
||||
FROM spbu"
|
||||
);
|
||||
$summary = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($summary) {
|
||||
$summary['server_time'] = date('Y-m-d H:i:s');
|
||||
}
|
||||
$summary = $stmt->fetch();
|
||||
sendSuccess($summary, 'Status SPBU berhasil diambil');
|
||||
} catch (PDOException $e) {
|
||||
sendError('Gagal mengambil status SPBU: ' . $e->getMessage(), 500);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
require_once __DIR__ . '/core_config/settings.php';
|
||||
|
||||
$host = getenv('DB_HOST') ?: 'localhost';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
require_once __DIR__ . '/core_config/session_mgr.php';
|
||||
startAppSession();
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
|
||||
+77
-663
@@ -37,680 +37,94 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id" data-theme="dark">
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login — WebGIS Smart City</title>
|
||||
<meta name="description" content="Masuk ke sistem WebGIS Smart City Pontianak.">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet">
|
||||
<title>Login - WebGIS Smart City</title>
|
||||
<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://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||
<style>
|
||||
/* ══ DESIGN TOKENS ══ */
|
||||
:root {
|
||||
--font-sans: 'Inter', sans-serif;
|
||||
--font-display: 'Space Grotesk', sans-serif;
|
||||
--ease: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition: all 0.25s var(--ease);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--bg-page: #0A0F1E;
|
||||
--bg-card: #111827;
|
||||
--bg-input: #1A2234;
|
||||
--border: rgba(255,255,255,0.08);
|
||||
--border-focus: #6366F1;
|
||||
--text-primary: #F1F5F9;
|
||||
--text-secondary:#94A3B8;
|
||||
--text-muted: #64748B;
|
||||
--primary: #6366F1;
|
||||
--primary-dark: #4F46E5;
|
||||
--primary-glow: rgba(99,102,241,0.25);
|
||||
--accent: #06B6D4;
|
||||
--error-bg: rgba(244,63,94,0.12);
|
||||
--error-border: rgba(244,63,94,0.3);
|
||||
--error-text: #FB7185;
|
||||
--role-border: rgba(255,255,255,0.08);
|
||||
--role-active-bg: rgba(99,102,241,0.15);
|
||||
--role-active-border: #6366F1;
|
||||
--shadow-card: 0 8px 48px rgba(0,0,0,0.5);
|
||||
--dot-color: rgba(255,255,255,0.03);
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
--bg-page: #EEF2FF;
|
||||
--bg-card: #FFFFFF;
|
||||
--bg-input: #F8FAFF;
|
||||
--border: #E2E8F0;
|
||||
--border-focus: #4F46E5;
|
||||
--text-primary: #0F172A;
|
||||
--text-secondary:#475569;
|
||||
--text-muted: #94A3B8;
|
||||
--primary: #4F46E5;
|
||||
--primary-dark: #3730A3;
|
||||
--primary-glow: rgba(79,70,229,0.12);
|
||||
--accent: #0891B2;
|
||||
--error-bg: #FFF1F2;
|
||||
--error-border: #FECDD3;
|
||||
--error-text: #BE123C;
|
||||
--role-border: #E2E8F0;
|
||||
--role-active-bg: #EEF2FF;
|
||||
--role-active-border: #4F46E5;
|
||||
--shadow-card: 0 8px 48px rgba(79,70,229,0.1);
|
||||
--dot-color: rgba(79,70,229,0.04);
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html { scroll-behavior: smooth; }
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--bg-page);
|
||||
color: var(--text-primary);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
transition: background 0.3s var(--ease), color 0.3s var(--ease);
|
||||
}
|
||||
|
||||
/* Dot grid */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-image: radial-gradient(circle, var(--dot-color) 1px, transparent 1px);
|
||||
background-size: 28px 28px;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Ambient glow */
|
||||
.orb {
|
||||
position: fixed;
|
||||
border-radius: 50%;
|
||||
filter: blur(100px);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
.orb-1 { width: 400px; height: 400px; background: var(--primary-glow); top: -80px; right: -80px; opacity: 0.7; }
|
||||
.orb-2 { width: 300px; height: 300px; background: rgba(6,182,212,0.08); bottom: -60px; left: -60px; opacity: 0.6; }
|
||||
|
||||
/* ── Top Bar ── */
|
||||
.topbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
width: 100%;
|
||||
padding: 0 24px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-card);
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: var(--transition);
|
||||
}
|
||||
.btn-back:hover {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: white;
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
.btn-back i { font-size: 0.75rem; }
|
||||
|
||||
.theme-toggle {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-card);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.95rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
.theme-toggle:hover {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* ── Main Layout ── */
|
||||
.login-page {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 24px;
|
||||
box-shadow: var(--shadow-card);
|
||||
overflow: hidden;
|
||||
animation: fadeIn 0.4s var(--ease) both;
|
||||
}
|
||||
|
||||
/* ── Left panel (branding) ── */
|
||||
.login-left {
|
||||
background: linear-gradient(145deg, var(--primary) 0%, #4338CA 60%, #312E81 100%);
|
||||
padding: 48px 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-left::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.06);
|
||||
bottom: -80px;
|
||||
right: -80px;
|
||||
}
|
||||
.login-left::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.04);
|
||||
top: -40px;
|
||||
left: -40px;
|
||||
}
|
||||
|
||||
.left-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background: rgba(255,255,255,0.15);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.left-body {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.left-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.left-desc {
|
||||
font-size: 0.9rem;
|
||||
color: rgba(255,255,255,0.7);
|
||||
line-height: 1.7;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 0.85rem;
|
||||
color: rgba(255,255,255,0.85);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255,255,255,0.12);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.85rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.left-footer {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-size: 0.75rem;
|
||||
color: rgba(255,255,255,0.4);
|
||||
}
|
||||
|
||||
/* ── Right panel (form) ── */
|
||||
.login-right {
|
||||
padding: 48px 44px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.form-header {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.form-subtitle {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* ── Role selector ── */
|
||||
.role-selector {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.role-card { position: relative; cursor: pointer; }
|
||||
|
||||
.role-card input[type="radio"] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0; height: 0;
|
||||
}
|
||||
|
||||
.role-card-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 14px 10px;
|
||||
border-radius: 12px;
|
||||
border: 1.5px solid var(--role-border);
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.role-card input:checked + .role-card-label {
|
||||
border-color: var(--role-active-border);
|
||||
background: var(--role-active-bg);
|
||||
}
|
||||
|
||||
.role-card-label:hover {
|
||||
border-color: var(--border-focus);
|
||||
background: var(--role-active-bg);
|
||||
}
|
||||
|
||||
.role-card-icon {
|
||||
font-size: 1.3rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.role-card input:checked + .role-card-label .role-card-icon {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.role-card-name {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.role-card input:checked + .role-card-label .role-card-name {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.role-card-desc {
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ── Error alert ── */
|
||||
.alert-error {
|
||||
background: var(--error-bg);
|
||||
border: 1px solid var(--error-border);
|
||||
border-radius: 10px;
|
||||
padding: 12px 16px;
|
||||
color: var(--error-text);
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
animation: shake 0.4s var(--ease);
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
20% { transform: translateX(-6px); }
|
||||
40% { transform: translateX(6px); }
|
||||
60% { transform: translateX(-4px); }
|
||||
80% { transform: translateX(4px); }
|
||||
}
|
||||
|
||||
/* ── Form fields ── */
|
||||
.field-group {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
display: block;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.field-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.field-icon {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
pointer-events: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.field-control {
|
||||
width: 100%;
|
||||
padding: 11px 14px 11px 40px;
|
||||
border-radius: 10px;
|
||||
border: 1.5px solid var(--border);
|
||||
background: var(--bg-input);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.9rem;
|
||||
font-family: var(--font-sans);
|
||||
outline: none;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.field-control:focus {
|
||||
border-color: var(--border-focus);
|
||||
box-shadow: 0 0 0 3px var(--primary-glow);
|
||||
}
|
||||
|
||||
.field-control:focus ~ .field-icon,
|
||||
.field-wrap:focus-within .field-icon {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.field-control::placeholder { color: var(--text-muted); }
|
||||
|
||||
.btn-eye {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.btn-eye:hover { color: var(--primary); }
|
||||
|
||||
/* ── Submit button ── */
|
||||
.btn-login {
|
||||
width: 100%;
|
||||
padding: 13px;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
||||
color: white;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-sans);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
transition: var(--transition);
|
||||
box-shadow: 0 4px 12px var(--primary-glow);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.btn-login:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px var(--primary-glow);
|
||||
filter: brightness(1.05);
|
||||
}
|
||||
|
||||
.btn-login:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* ── Animations ── */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: scale(0.97) translateY(12px); }
|
||||
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||
}
|
||||
|
||||
/* ── Responsive ── */
|
||||
@media (max-width: 768px) {
|
||||
.login-container { grid-template-columns: 1fr; }
|
||||
.login-left { display: none; }
|
||||
.login-right { padding: 36px 28px; }
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="public_assets/css/global.css">
|
||||
<link rel="stylesheet" href="public_assets/css/auth.css">
|
||||
</head>
|
||||
<body>
|
||||
<body class="auth-page">
|
||||
|
||||
<div class="orb orb-1"></div>
|
||||
<div class="orb orb-2"></div>
|
||||
<div class="auth-center">
|
||||
<div class="auth-form-wrap">
|
||||
<h2 class="auth-form-title">Selamat Datang</h2>
|
||||
<p class="auth-form-subtitle">Masuk ke sistem WebGIS Smart City Pontianak.</p>
|
||||
|
||||
<!-- Top bar -->
|
||||
<div class="topbar">
|
||||
<a href="../portal.html" class="btn-back" id="backToPortal">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
Kembali ke Portal
|
||||
</a>
|
||||
<button class="theme-toggle" id="themeToggle" title="Toggle tema" aria-label="Toggle dark/light mode">
|
||||
<i class="fas fa-moon" id="themeIcon"></i>
|
||||
</button>
|
||||
<div class="role-selector">
|
||||
<label class="role-card">
|
||||
<input type="radio" name="role_display" value="admin" checked>
|
||||
<div class="role-card-label">
|
||||
<div class="role-card-icon"><i class="fas fa-user-shield"></i></div>
|
||||
<div class="role-card-name">Admin</div>
|
||||
<div class="role-card-desc">Kelola semua data</div>
|
||||
</div>
|
||||
</label>
|
||||
<label class="role-card">
|
||||
<input type="radio" name="role_display" value="user">
|
||||
<div class="role-card-label">
|
||||
<div class="role-card-icon"><i class="fas fa-user"></i></div>
|
||||
<div class="role-card-name">Pengguna</div>
|
||||
<div class="role-card-desc">Lihat & analisis peta</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="auth-error">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<?= htmlspecialchars($error) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<form method="POST" class="auth-form">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Username</label>
|
||||
<div class="input-with-icon">
|
||||
<i class="fas fa-user input-icon"></i>
|
||||
<input type="text" name="username" class="form-control"
|
||||
placeholder="Masukkan username"
|
||||
value="<?= htmlspecialchars($_POST['username'] ?? '') ?>"
|
||||
required autocomplete="username">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Password</label>
|
||||
<div class="input-with-icon">
|
||||
<i class="fas fa-lock input-icon"></i>
|
||||
<input type="password" name="password" class="form-control"
|
||||
placeholder="Masukkan password"
|
||||
required autocomplete="current-password"
|
||||
id="passwordInput">
|
||||
<button type="button" class="input-icon" style="right:12px;left:auto;cursor:pointer;background:none;border:none;color:var(--text-muted);" onclick="togglePass()">
|
||||
<i class="fas fa-eye" id="eyeIcon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-login">
|
||||
<i class="fas fa-sign-in-alt"></i>
|
||||
Masuk ke Sistem
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Login page -->
|
||||
<main class="login-page">
|
||||
<div class="login-container">
|
||||
|
||||
<!-- Left branding -->
|
||||
<div class="login-left">
|
||||
<div class="left-brand">
|
||||
<div class="brand-icon"><i class="fas fa-map-marked-alt"></i></div>
|
||||
<span class="brand-name">WebGIS Smart City</span>
|
||||
</div>
|
||||
|
||||
<div class="left-body">
|
||||
<h2 class="left-title">Selamat Datang di<br>Sistem Informasi Geografis</h2>
|
||||
<p class="left-desc">Platform manajemen data spasial kawasan kota Pontianak dengan visualisasi peta interaktif real-time.</p>
|
||||
|
||||
<div class="feature-list">
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon"><i class="fas fa-map"></i></div>
|
||||
Peta Interaktif Real-time
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon"><i class="fas fa-chart-bar"></i></div>
|
||||
Dashboard Analitik
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon"><i class="fas fa-shield-alt"></i></div>
|
||||
Autentikasi Multi-Peran
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="left-footer">
|
||||
D1041231071 — Naufal Zaky Ramadhan © 2026
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right form -->
|
||||
<div class="login-right">
|
||||
<div class="form-header">
|
||||
<h1 class="form-title">Masuk ke Sistem</h1>
|
||||
<p class="form-subtitle">Pilih peran dan masukkan kredensial Anda.</p>
|
||||
</div>
|
||||
|
||||
<!-- Role selector -->
|
||||
<div class="role-selector">
|
||||
<label class="role-card">
|
||||
<input type="radio" name="role_display" value="admin" checked>
|
||||
<div class="role-card-label">
|
||||
<div class="role-card-icon"><i class="fas fa-user-shield"></i></div>
|
||||
<div class="role-card-name">Admin</div>
|
||||
<div class="role-card-desc">Kelola semua data</div>
|
||||
</div>
|
||||
</label>
|
||||
<label class="role-card">
|
||||
<input type="radio" name="role_display" value="user">
|
||||
<div class="role-card-label">
|
||||
<div class="role-card-icon"><i class="fas fa-user"></i></div>
|
||||
<div class="role-card-name">Pengguna</div>
|
||||
<div class="role-card-desc">Lihat & analisis peta</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert-error">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<?= htmlspecialchars($error) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST" autocomplete="on">
|
||||
<div class="field-group">
|
||||
<label class="field-label" for="usernameInput">Username</label>
|
||||
<div class="field-wrap">
|
||||
<i class="fas fa-user field-icon"></i>
|
||||
<input type="text" name="username" id="usernameInput" class="field-control"
|
||||
placeholder="Masukkan username"
|
||||
value="<?= htmlspecialchars($_POST['username'] ?? '') ?>"
|
||||
required autocomplete="username">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<label class="field-label" for="passwordInput">Password</label>
|
||||
<div class="field-wrap">
|
||||
<i class="fas fa-lock field-icon"></i>
|
||||
<input type="password" name="password" id="passwordInput" class="field-control"
|
||||
placeholder="Masukkan password"
|
||||
required autocomplete="current-password">
|
||||
<button type="button" class="btn-eye" onclick="togglePass()" aria-label="Toggle password visibility">
|
||||
<i class="fas fa-eye" id="eyeIcon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-login" id="loginBtn">
|
||||
<i class="fas fa-sign-in-alt"></i>
|
||||
Masuk ke Sistem
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// ── Password toggle ──
|
||||
function togglePass() {
|
||||
const inp = document.getElementById('passwordInput');
|
||||
const ico = document.getElementById('eyeIcon');
|
||||
const isPass = inp.type === 'password';
|
||||
inp.type = isPass ? 'text' : 'password';
|
||||
ico.className = isPass ? 'fas fa-eye-slash' : 'fas fa-eye';
|
||||
function togglePass() {
|
||||
const inp = document.getElementById('passwordInput');
|
||||
const ico = document.getElementById('eyeIcon');
|
||||
if (inp.type === 'password') {
|
||||
inp.type = 'text';
|
||||
ico.className = 'fas fa-eye-slash';
|
||||
} else {
|
||||
inp.type = 'password';
|
||||
ico.className = 'fas fa-eye';
|
||||
}
|
||||
|
||||
// ── Theme toggle ──
|
||||
const html = document.documentElement;
|
||||
const btn = document.getElementById('themeToggle');
|
||||
const icon = document.getElementById('themeIcon');
|
||||
|
||||
const saved = localStorage.getItem('portal-theme') || 'dark';
|
||||
setTheme(saved);
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
const next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
||||
setTheme(next);
|
||||
localStorage.setItem('portal-theme', next);
|
||||
});
|
||||
|
||||
function setTheme(t) {
|
||||
html.setAttribute('data-theme', t);
|
||||
icon.className = t === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
|
||||
}
|
||||
|
||||
// ── Loading state on submit ──
|
||||
document.querySelector('form').addEventListener('submit', function() {
|
||||
const loginBtn = document.getElementById('loginBtn');
|
||||
loginBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Memproses...';
|
||||
loginBtn.disabled = true;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
require_once __DIR__ . '/core_config/session_mgr.php';
|
||||
startAppSession();
|
||||
session_destroy();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
require_once __DIR__ . '/../core_config/database.php';
|
||||
require_once __DIR__ . '/../core_config/middleware_auth.php';
|
||||
requireRole('admin');
|
||||
@@ -32,7 +32,7 @@ $blankCount = $pdo->query("
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM rumah_ibadah ri
|
||||
WHERE ST_Distance_Sphere(w.geom, ri.geom) <= ri.radius_bantuan_meter
|
||||
WHERE ST_Distance_Sphere(ST_GeomFromText(ST_AsText(w.geom), 4326), ST_GeomFromText(ST_AsText(ri.geom), 4326)) <= ri.radius_bantuan_meter
|
||||
)
|
||||
")->fetch();
|
||||
$blankSpot = (int)($blankCount['cnt'] ?? 0);
|
||||
@@ -47,11 +47,11 @@ require_once __DIR__ . '/partials/header.php';
|
||||
?>
|
||||
|
||||
<!-- ── Quick Access Map ── -->
|
||||
<div class="card" style="background: var(--primary-glow); border: 1px solid var(--primary-light); margin-bottom: 24px;">
|
||||
<div class="card" style="background: #EFF6FF; border: 1px solid #BFDBFE; margin-bottom: 24px;">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:16px;">
|
||||
<div>
|
||||
<h3 style="font-size:1.1rem; font-weight:700; margin-bottom:6px; color:var(--text-primary);">🗺️ Buka Peta Interaktif</h3>
|
||||
<p style="color:var(--text-secondary); font-size:0.88rem; max-width:500px;">
|
||||
<h3 style="font-size:1.1rem; font-weight:700; margin-bottom:6px; color:#1E3A8A;">🗺️ Buka Peta Interaktif</h3>
|
||||
<p style="color:#3B82F6; font-size:0.88rem; max-width:500px;">
|
||||
Tambahkan, edit, dan analisis data spasial langsung di peta. Mendukung drawing tool untuk semua layer data.
|
||||
</p>
|
||||
</div>
|
||||
@@ -127,9 +127,9 @@ require_once __DIR__ . '/partials/header.php';
|
||||
<?php if ($recentSpbu): ?>
|
||||
<div style="display:flex; flex-direction:column; gap:8px;">
|
||||
<?php foreach ($recentSpbu as $s): ?>
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; padding:10px 12px; background:var(--bg-elevated); border:1px solid var(--border-light); border-radius:8px;">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; padding:10px 12px; background:#F9FAFB; border:1px solid #E5E7EB; border-radius:8px;">
|
||||
<div>
|
||||
<div style="font-weight:600; font-size:0.88rem; color:var(--text-primary);"><?= htmlspecialchars($s['nama']) ?></div>
|
||||
<div style="font-weight:600; font-size:0.88rem;"><?= htmlspecialchars($s['nama']) ?></div>
|
||||
<div style="font-size:0.75rem; color:var(--text-muted);"><?= date('d M Y', strtotime($s['created_at'])) ?></div>
|
||||
</div>
|
||||
<span class="badge <?= $s['buka_24_jam'] ? 'badge-success' : 'badge-danger' ?>">
|
||||
@@ -152,9 +152,9 @@ require_once __DIR__ . '/partials/header.php';
|
||||
<?php if ($recentWarga): ?>
|
||||
<div style="display:flex; flex-direction:column; gap:8px;">
|
||||
<?php foreach ($recentWarga as $w): ?>
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; padding:10px 12px; background:var(--bg-elevated); border:1px solid var(--border-light); border-radius:8px;">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; padding:10px 12px; background:#F9FAFB; border:1px solid #E5E7EB; border-radius:8px;">
|
||||
<div>
|
||||
<div style="font-weight:600; font-size:0.88rem; color:var(--text-primary);"><?= htmlspecialchars($w['nama_kk']) ?></div>
|
||||
<div style="font-weight:600; font-size:0.88rem;"><?= htmlspecialchars($w['nama_kk']) ?></div>
|
||||
<div style="font-size:0.75rem; color:var(--text-muted);">Rp <?= number_format($w['penghasilan'],0,',','.') ?>/bln · <?= $w['jumlah_tanggungan'] ?> tanggungan</div>
|
||||
</div>
|
||||
<span class="badge badge-warning"><?= $w['jumlah_tanggungan'] ?> org</span>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
require_once __DIR__ . '/../core_config/database.php';
|
||||
require_once __DIR__ . '/../core_config/middleware_auth.php';
|
||||
requireRole('admin');
|
||||
@@ -64,10 +64,10 @@ foreach($rows as $r) {
|
||||
</div>
|
||||
<div class="form-group"><label class="form-label">Radius Bantuan (meter) *</label><input type="number" name="radius_bantuan_meter" class="form-control" min="100" max="10000" step="50" value="1000" required></div>
|
||||
<div class="form-group"><label class="form-label">Pilih Lokasi di Peta</label>
|
||||
<div id="miniMapTambah" style="height:200px;border-radius:8px;border:1px solid var(--border-light);margin-bottom:10px;z-index:1;"></div>
|
||||
<div id="miniMapTambah" style="height:200px;border-radius:8px;border:1px solid #E5E7EB;margin-bottom:10px;z-index:1;"></div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
||||
<input type="number" name="lat" id="latTambah" class="form-control" placeholder="Latitude" step="any" readonly style="background:var(--bg-base); border-color:var(--border-light);">
|
||||
<input type="number" name="lng" id="lngTambah" class="form-control" placeholder="Longitude" step="any" readonly style="background:var(--bg-base); border-color:var(--border-light);">
|
||||
<input type="number" name="lat" id="latTambah" class="form-control" placeholder="Latitude" step="any" readonly style="background:#F9FAFB;">
|
||||
<input type="number" name="lng" id="lngTambah" class="form-control" placeholder="Longitude" step="any" readonly style="background:#F9FAFB;">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -104,11 +104,7 @@ window.openModal = function(id) {
|
||||
setTimeout(() => {
|
||||
if(!miniMap) {
|
||||
miniMap = L.map('miniMapTambah').setView([-0.0583, 109.3448], 13);
|
||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
const tileUrl = isDark
|
||||
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
|
||||
: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png';
|
||||
L.tileLayer(tileUrl).addTo(miniMap);
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png').addTo(miniMap);
|
||||
miniMap.on('click', function(e) {
|
||||
const lat = e.latlng.lat;
|
||||
const lng = e.latlng.lng;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
require_once __DIR__ . '/../core_config/database.php';
|
||||
require_once __DIR__ . '/../core_config/middleware_auth.php';
|
||||
requireRole('admin');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
require_once __DIR__ . '/../core_config/database.php';
|
||||
require_once __DIR__ . '/../core_config/middleware_auth.php';
|
||||
requireRole('admin');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
require_once __DIR__ . '/../core_config/database.php';
|
||||
require_once __DIR__ . '/../core_config/middleware_auth.php';
|
||||
requireRole('admin');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
require_once __DIR__ . '/../core_config/database.php';
|
||||
require_once __DIR__ . '/../core_config/middleware_auth.php';
|
||||
requireRole('admin');
|
||||
@@ -113,7 +113,7 @@ function badgeStatus($status) {
|
||||
<strong id="mapKategori"></strong><br>
|
||||
<span style="color: var(--text-secondary);" id="mapDeskripsi"></span>
|
||||
</div>
|
||||
<div id="miniMapLihat" style="height:300px;border-radius:8px;border:1px solid var(--border-light);"></div>
|
||||
<div id="miniMapLihat" style="height:300px;border-radius:8px;border:1px solid #E5E7EB;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -177,11 +177,7 @@ function lihatLokasi(r) {
|
||||
setTimeout(() => {
|
||||
if(!mapInstance) {
|
||||
mapInstance = L.map('miniMapLihat');
|
||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
const tileUrl = isDark
|
||||
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
|
||||
: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png';
|
||||
L.tileLayer(tileUrl).addTo(mapInstance);
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png').addTo(mapInstance);
|
||||
}
|
||||
if(mapMarker) mapInstance.removeLayer(mapMarker);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
require_once __DIR__ . '/../core_config/database.php';
|
||||
require_once __DIR__ . '/../core_config/middleware_auth.php';
|
||||
requireRole('admin');
|
||||
@@ -59,10 +59,10 @@ require_once __DIR__ . '/partials/header.php';
|
||||
<div class="form-group"><label class="form-label" style="display:flex;align-items:center;gap:10px;cursor:pointer;"><input type="checkbox" name="buka_24_jam" id="cb24jam" style="width:16px;height:16px;accent-color:var(--accent);"> Buka 24 Jam</label></div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Pilih Lokasi di Peta *</label>
|
||||
<div id="miniMapTambah" style="height:200px;border-radius:8px;border:1px solid var(--border-light);margin-bottom:10px;z-index:1;"></div>
|
||||
<div id="miniMapTambah" style="height:200px;border-radius:8px;border:1px solid #E5E7EB;margin-bottom:10px;z-index:1;"></div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
||||
<input type="number" name="lat" id="latTambah" class="form-control" placeholder="Latitude" step="any" required readonly style="background:var(--bg-base); border-color:var(--border-light);">
|
||||
<input type="number" name="lng" id="lngTambah" class="form-control" placeholder="Longitude" step="any" required readonly style="background:var(--bg-base); border-color:var(--border-light);">
|
||||
<input type="number" name="lat" id="latTambah" class="form-control" placeholder="Latitude" step="any" required readonly style="background:#F9FAFB;">
|
||||
<input type="number" name="lng" id="lngTambah" class="form-control" placeholder="Longitude" step="any" required readonly style="background:#F9FAFB;">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -104,11 +104,7 @@ window.openModal = function(id) {
|
||||
setTimeout(() => {
|
||||
if(!miniMap) {
|
||||
miniMap = L.map('miniMapTambah').setView([-0.0583, 109.3448], 13);
|
||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
const tileUrl = isDark
|
||||
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
|
||||
: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png';
|
||||
L.tileLayer(tileUrl).addTo(miniMap);
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png').addTo(miniMap);
|
||||
miniMap.on('click', function(e) {
|
||||
const lat = e.latlng.lat;
|
||||
const lng = e.latlng.lng;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
require_once __DIR__ . '/../core_config/database.php';
|
||||
require_once __DIR__ . '/../core_config/middleware_auth.php';
|
||||
requireRole('admin');
|
||||
@@ -45,10 +45,10 @@ require_once __DIR__ . '/partials/header.php';
|
||||
<div class="form-group"><label class="form-label">Penghasilan per Bulan (Rp)</label><input type="number" name="penghasilan" class="form-control" placeholder="0" min="0"></div>
|
||||
<div class="form-group"><label class="form-label">Jumlah Tanggungan (jiwa)</label><input type="number" name="jumlah_tanggungan" class="form-control" placeholder="0" min="0" max="20"></div>
|
||||
<div class="form-group"><label class="form-label">Pilih Lokasi di Peta *</label>
|
||||
<div id="miniMapTambah" style="height:200px;border-radius:8px;border:1px solid var(--border-light);margin-bottom:10px;z-index:1;"></div>
|
||||
<div id="miniMapTambah" style="height:200px;border-radius:8px;border:1px solid #E5E7EB;margin-bottom:10px;z-index:1;"></div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
||||
<input type="number" name="lat" id="latTambah" class="form-control" placeholder="Latitude" step="any" required readonly style="background:var(--bg-base); border-color:var(--border-light);">
|
||||
<input type="number" name="lng" id="lngTambah" class="form-control" placeholder="Longitude" step="any" required readonly style="background:var(--bg-base); border-color:var(--border-light);">
|
||||
<input type="number" name="lat" id="latTambah" class="form-control" placeholder="Latitude" step="any" required readonly style="background:#F9FAFB;">
|
||||
<input type="number" name="lng" id="lngTambah" class="form-control" placeholder="Longitude" step="any" required readonly style="background:#F9FAFB;">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -77,11 +77,7 @@ window.openModal = function(id) {
|
||||
setTimeout(() => {
|
||||
if(!miniMap) {
|
||||
miniMap = L.map('miniMapTambah').setView([-0.0583, 109.3448], 13);
|
||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
const tileUrl = isDark
|
||||
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
|
||||
: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png';
|
||||
L.tileLayer(tileUrl).addTo(miniMap);
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png').addTo(miniMap);
|
||||
miniMap.on('click', function(e) {
|
||||
const lat = e.latlng.lat;
|
||||
const lng = e.latlng.lng;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
require_once __DIR__ . '/../core_config/database.php';
|
||||
require_once __DIR__ . '/../core_config/middleware_auth.php';
|
||||
requireRole('admin');
|
||||
|
||||
@@ -5,32 +5,6 @@
|
||||
<div class="toast-container" id="toastContainer"></div>
|
||||
|
||||
<script>
|
||||
// ── Theme toggle ──
|
||||
(function() {
|
||||
const html = document.documentElement;
|
||||
const saved = localStorage.getItem('portal-theme') || 'dark';
|
||||
setAdminTheme(saved);
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const btn = document.getElementById('adminThemeToggle');
|
||||
const icon = document.getElementById('adminThemeIcon');
|
||||
if (!btn) return;
|
||||
btn.addEventListener('click', () => {
|
||||
const next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
||||
setAdminTheme(next);
|
||||
localStorage.setItem('portal-theme', next);
|
||||
});
|
||||
function updateIcon() {
|
||||
if (icon) icon.className = html.getAttribute('data-theme') === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
|
||||
}
|
||||
updateIcon();
|
||||
});
|
||||
function setAdminTheme(t) {
|
||||
html.setAttribute('data-theme', t);
|
||||
const icon = document.getElementById('adminThemeIcon');
|
||||
if (icon) icon.className = t === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
|
||||
}
|
||||
})();
|
||||
|
||||
// ── Clock ──
|
||||
function updateClock() {
|
||||
const now = new Date();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
/**
|
||||
* Admin Layout — sidebar + topbar
|
||||
* Include di awal setiap halaman admin setelah auth check.
|
||||
@@ -8,7 +8,7 @@ $user = currentUser();
|
||||
$initial = strtoupper(substr($user['nama_lengkap'] ?: $user['username'], 0, 1));
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id" data-theme="dark">
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -105,12 +105,6 @@ $initial = strtoupper(substr($user['nama_lengkap'] ?: $user['username'], 0, 1));
|
||||
<a href="<?= app_url('panel_admin/peta_admin.php') ?>" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-map"></i> Buka Peta
|
||||
</a>
|
||||
<button class="btn btn-ghost btn-sm" id="adminThemeToggle" title="Toggle tema" aria-label="Toggle dark/light mode" style="padding:8px 10px;">
|
||||
<i class="fas fa-moon" id="adminThemeIcon"></i>
|
||||
</button>
|
||||
<a href="<?= app_url('../portal.html') ?>" class="btn btn-ghost btn-sm" title="Kembali ke Portal WebGIS">
|
||||
<i class="fas fa-home"></i> Portal
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-content">
|
||||
|
||||
@@ -30,7 +30,7 @@ require_once __DIR__ . '/partials/header.php';
|
||||
<div class="layer-section-title">Infrastruktur</div>
|
||||
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" id="ly-spbu">
|
||||
<input type="checkbox" id="ly-spbu" checked>
|
||||
<span class="layer-color layer-point" style="--layer-color:#F59E0B"><i class="fas fa-gas-pump"></i></span>
|
||||
<span class="layer-text">
|
||||
<span class="layer-name">SPBU</span>
|
||||
@@ -39,7 +39,7 @@ require_once __DIR__ . '/partials/header.php';
|
||||
</label>
|
||||
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" id="ly-jalan">
|
||||
<input type="checkbox" id="ly-jalan" checked>
|
||||
<span class="layer-color layer-line" style="--layer-color:#3B82F6"></span>
|
||||
<span class="layer-text">
|
||||
<span class="layer-name">Jalan</span>
|
||||
@@ -48,7 +48,7 @@ require_once __DIR__ . '/partials/header.php';
|
||||
</label>
|
||||
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" id="ly-kavling">
|
||||
<input type="checkbox" id="ly-kavling" checked>
|
||||
<span class="layer-color layer-area" style="--layer-color:#7C3AED"></span>
|
||||
<span class="layer-text">
|
||||
<span class="layer-name">Kavling / Parsil</span>
|
||||
@@ -168,6 +168,6 @@ require_once __DIR__ . '/partials/header.php';
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
|
||||
<script src="<?= app_url('public_assets/js/peta-admin.js') ?>?v=<?= time() ?>"></script>
|
||||
<script src="<?= app_url('public_assets/js/peta-admin.js') ?>?v=20260612-fix"></script>
|
||||
<?php $extraScript = ''; ?>
|
||||
<?php require_once __DIR__ . '/partials/footer.php'; ?>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
require_once __DIR__ . '/../core_config/database.php';
|
||||
require_once __DIR__ . '/../core_config/middleware_auth.php';
|
||||
requireRole('user');
|
||||
@@ -107,10 +107,10 @@ function badgeStatus($status) {
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Pilih Lokasi di Peta *</label>
|
||||
<div id="miniMapTambah" style="height:200px;border-radius:8px;border:1px solid var(--border-light);margin-bottom:10px;z-index:1;"></div>
|
||||
<div id="miniMapTambah" style="height:200px;border-radius:8px;border:1px solid #E5E7EB;margin-bottom:10px;z-index:1;"></div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
||||
<input type="number" name="lat" id="latTambah" class="form-control" placeholder="Latitude" step="any" required readonly style="background:var(--bg-base); border-color:var(--border-light);">
|
||||
<input type="number" name="lng" id="lngTambah" class="form-control" placeholder="Longitude" step="any" required readonly style="background:var(--bg-base); border-color:var(--border-light);">
|
||||
<input type="number" name="lat" id="latTambah" class="form-control" placeholder="Latitude" step="any" required readonly style="background:#F9FAFB;">
|
||||
<input type="number" name="lng" id="lngTambah" class="form-control" placeholder="Longitude" step="any" required readonly style="background:#F9FAFB;">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -134,11 +134,7 @@ window.openModal = function(id) {
|
||||
setTimeout(() => {
|
||||
if(!miniMap) {
|
||||
miniMap = L.map('miniMapTambah').setView([-0.0583, 109.3448], 13);
|
||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
const tileUrl = isDark
|
||||
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
|
||||
: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png';
|
||||
L.tileLayer(tileUrl).addTo(miniMap);
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png').addTo(miniMap);
|
||||
miniMap.on('click', function(e) {
|
||||
const lat = e.latlng.lat;
|
||||
const lng = e.latlng.lng;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
require_once __DIR__ . '/../core_config/database.php';
|
||||
require_once __DIR__ . '/../core_config/middleware_auth.php';
|
||||
requireRole('user');
|
||||
@@ -29,7 +29,7 @@ $blankCount = $pdo->query("
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM rumah_ibadah ri
|
||||
WHERE ST_Distance_Sphere(w.geom, ri.geom) <= ri.radius_bantuan_meter
|
||||
WHERE ST_Distance_Sphere(ST_GeomFromText(ST_AsText(w.geom), 4326), ST_GeomFromText(ST_AsText(ri.geom), 4326)) <= ri.radius_bantuan_meter
|
||||
)
|
||||
")->fetch();
|
||||
$blankSpot = (int)($blankCount['cnt'] ?? 0);
|
||||
@@ -39,7 +39,7 @@ $activeNav = 'dashboard';
|
||||
require_once __DIR__ . '/partials/header.php';
|
||||
?>
|
||||
|
||||
<div class="card" style="background: var(--bg-surface); border: 1px solid var(--border-light); margin-bottom: 24px;">
|
||||
<div class="card" style="background: #FFFFFF; border: 1px solid #E5E7EB; border-radius: var(--radius-lg); padding: 24px; margin-bottom: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.05);">
|
||||
<h2 style="font-size:1.4rem;font-weight:700;margin-bottom:6px;color:var(--text-primary);">👋 Selamat Datang, <?= htmlspecialchars(currentUser()['nama_lengkap'] ?: currentUser()['username']) ?>!</h2>
|
||||
<p style="color:var(--text-secondary);font-size:0.9rem;max-width:600px;">
|
||||
Anda masuk sebagai <strong>Pengguna</strong>. Anda dapat melihat seluruh data spasial, analisis, dan berpartisipasi dengan mengirim Laporan Warga atau memberikan ulasan fasilitas.
|
||||
@@ -97,10 +97,10 @@ require_once __DIR__ . '/partials/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="background: var(--primary-glow); border: 1px solid var(--primary-light); margin-bottom: 24px;">
|
||||
<div class="card" style="background: #FFFFFF; border: 1px solid #E5E7EB; border-radius: var(--radius-lg); padding: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.05);">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:16px;">
|
||||
<div>
|
||||
<h3 style="font-size:1.1rem; font-weight:700; margin-bottom:6px; color:var(--text-primary);">🗺️ Buka Peta Interaktif</h3>
|
||||
<h3 style="font-size:1.1rem; font-weight:700; margin-bottom:6px;">🗺️ Buka Peta Interaktif</h3>
|
||||
<p style="color:var(--text-secondary); font-size:0.88rem; max-width:500px;">
|
||||
Lihat semua layer data spasial: SPBU, jalan, kavling, rumah ibadah, warga miskin, kawasan kumuh, dan analisis spasial.
|
||||
</p>
|
||||
|
||||
@@ -5,32 +5,6 @@
|
||||
<div class="toast-container" id="toastContainer"></div>
|
||||
|
||||
<script>
|
||||
// ── Theme toggle ──
|
||||
(function() {
|
||||
const html = document.documentElement;
|
||||
const saved = localStorage.getItem('portal-theme') || 'dark';
|
||||
setUserTheme(saved);
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const btn = document.getElementById('userThemeToggle');
|
||||
const icon = document.getElementById('userThemeIcon');
|
||||
if (!btn) return;
|
||||
btn.addEventListener('click', () => {
|
||||
const next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
||||
setUserTheme(next);
|
||||
localStorage.setItem('portal-theme', next);
|
||||
});
|
||||
function updateIcon() {
|
||||
if (icon) icon.className = html.getAttribute('data-theme') === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
|
||||
}
|
||||
updateIcon();
|
||||
});
|
||||
function setUserTheme(t) {
|
||||
html.setAttribute('data-theme', t);
|
||||
const icon = document.getElementById('userThemeIcon');
|
||||
if (icon) icon.className = t === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
|
||||
}
|
||||
})();
|
||||
|
||||
// ── Clock ──
|
||||
function updateClock() {
|
||||
const now = new Date();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
/**
|
||||
* User Layout — sidebar + topbar (read-only mode, beda style dari admin)
|
||||
* Variables expected from parent: $pageTitle, $activeNav
|
||||
@@ -7,7 +7,7 @@ $user = currentUser();
|
||||
$initial = strtoupper(substr($user['nama_lengkap'] ?: $user['username'], 0, 1));
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id" data-theme="dark">
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -80,12 +80,6 @@ $initial = strtoupper(substr($user['nama_lengkap'] ?: $user['username'], 0, 1));
|
||||
<span class="badge badge-success" style="font-size:0.7rem;padding:5px 10px;">
|
||||
<i class="fas fa-eye"></i> Mode Baca
|
||||
</span>
|
||||
<button class="btn btn-ghost btn-sm" id="userThemeToggle" title="Toggle tema" aria-label="Toggle dark/light mode" style="padding:8px 10px;">
|
||||
<i class="fas fa-moon" id="userThemeIcon"></i>
|
||||
</button>
|
||||
<a href="<?= app_url('../portal.html') ?>" class="btn btn-ghost btn-sm" title="Kembali ke Portal WebGIS">
|
||||
<i class="fas fa-home"></i> Portal
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-content">
|
||||
|
||||
@@ -33,7 +33,7 @@ require_once __DIR__ . '/partials/header.php';
|
||||
<div class="layer-section-title">Infrastruktur</div>
|
||||
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" id="ly-spbu">
|
||||
<input type="checkbox" id="ly-spbu" checked>
|
||||
<span class="layer-color layer-point" style="--layer-color:#F59E0B"><i class="fas fa-gas-pump"></i></span>
|
||||
<span class="layer-text">
|
||||
<span class="layer-name">SPBU</span>
|
||||
@@ -42,7 +42,7 @@ require_once __DIR__ . '/partials/header.php';
|
||||
</label>
|
||||
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" id="ly-jalan">
|
||||
<input type="checkbox" id="ly-jalan" checked>
|
||||
<span class="layer-color layer-line" style="--layer-color:#3B82F6"></span>
|
||||
<span class="layer-text">
|
||||
<span class="layer-name">Jalan</span>
|
||||
@@ -51,7 +51,7 @@ require_once __DIR__ . '/partials/header.php';
|
||||
</label>
|
||||
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" id="ly-kavling">
|
||||
<input type="checkbox" id="ly-kavling" checked>
|
||||
<span class="layer-color layer-area" style="--layer-color:#7C3AED"></span>
|
||||
<span class="layer-text">
|
||||
<span class="layer-name">Kavling / Parsil</span>
|
||||
@@ -156,6 +156,6 @@ require_once __DIR__ . '/partials/header.php';
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="<?= app_url('public_assets/js/peta-user.js') ?>?v=<?= time() ?>"></script>
|
||||
<script src="<?= app_url('public_assets/js/peta-user.js') ?>?v=20260612-fix"></script>
|
||||
<?php $extraScript = ''; ?>
|
||||
<?php require_once __DIR__ . '/partials/footer.php'; ?>
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
/* =========================================================
|
||||
WEBGIS PROJECT FINAL — Design System (global.css)
|
||||
WEBGIS PROJECT FINAL — Design System (main.css)
|
||||
========================================================= */
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
|
||||
|
||||
/* ── Light Theme (default) ── */
|
||||
:root, [data-theme="light"] {
|
||||
/* Colors */
|
||||
--primary: #2563EB;
|
||||
:root {
|
||||
/* Colors - Clean Light Theme */
|
||||
--primary: #2563EB; /* Professional Blue */
|
||||
--primary-light: #3B82F6;
|
||||
--primary-dark: #1D4ED8;
|
||||
--primary-glow: rgba(37, 99, 235, 0.08);
|
||||
--accent: #10B981;
|
||||
--danger: #EF4444;
|
||||
--warning: #F59E0B;
|
||||
--info: #0EA5E9;
|
||||
|
||||
/* Backgrounds */
|
||||
--bg-base: #F9FAFB;
|
||||
--bg-surface: #FFFFFF;
|
||||
--bg-base: #F9FAFB; /* Very light gray */
|
||||
--bg-surface: #FFFFFF; /* Pure white */
|
||||
--bg-elevated: #FFFFFF;
|
||||
--bg-card: #FFFFFF;
|
||||
|
||||
@@ -27,9 +25,9 @@
|
||||
--border-hover: #D1D5DB;
|
||||
|
||||
/* Text */
|
||||
--text-primary: #111827;
|
||||
--text-secondary: #4B5563;
|
||||
--text-muted: #6B7280;
|
||||
--text-primary: #111827; /* Near black */
|
||||
--text-secondary: #4B5563; /* Dark gray */
|
||||
--text-muted: #6B7280; /* Medium gray */
|
||||
|
||||
/* Sidebar */
|
||||
--sidebar-w: 260px;
|
||||
@@ -44,34 +42,7 @@
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
--transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* ── Dark Theme ── */
|
||||
[data-theme="dark"] {
|
||||
--primary: #818CF8;
|
||||
--primary-light: #A5B4FC;
|
||||
--primary-dark: #6366F1;
|
||||
--primary-glow: rgba(129, 140, 248, 0.15);
|
||||
--accent: #34D399;
|
||||
--danger: #F87171;
|
||||
--warning: #FCD34D;
|
||||
--info: #38BDF8;
|
||||
|
||||
--bg-base: #0F172A;
|
||||
--bg-surface: #1E293B;
|
||||
--bg-elevated: #293548;
|
||||
--bg-card: #1E293B;
|
||||
|
||||
--border-light: rgba(255,255,255,0.08);
|
||||
--border-hover: rgba(129,140,248,0.45);
|
||||
|
||||
--text-primary: #F1F5F9;
|
||||
--text-secondary: #94A3B8;
|
||||
--text-muted: #64748B;
|
||||
|
||||
--sidebar-bg: #0F172A;
|
||||
--sidebar-border: rgba(255,255,255,0.07);
|
||||
--transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
@@ -86,7 +57,6 @@ body {
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
transition: background 0.25s ease, color 0.25s ease;
|
||||
}
|
||||
|
||||
a { color: var(--primary); text-decoration: none; transition: var(--transition); }
|
||||
@@ -97,7 +67,6 @@ a:hover { color: var(--primary-dark); }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: #CBD5E1; border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #94A3B8; }
|
||||
[data-theme="dark"] ::-webkit-scrollbar-thumb { background: #334155; }
|
||||
|
||||
/* ── Buttons ── */
|
||||
.btn {
|
||||
@@ -114,7 +83,7 @@ a:hover { color: var(--primary-dark); }
|
||||
.btn-danger { background: var(--danger); color: #fff; }
|
||||
.btn-danger:hover { filter: brightness(0.95); }
|
||||
.btn-ghost { background: transparent; border: 1px solid var(--border-light); color: var(--text-secondary); }
|
||||
.btn-ghost:hover { background: var(--bg-elevated); color: var(--text-primary); border-color: var(--border-hover); }
|
||||
.btn-ghost:hover { background: #F3F4F6; color: var(--text-primary); border-color: var(--border-hover); }
|
||||
.btn-sm { padding: 6px 12px; font-size: 0.78rem; }
|
||||
.btn-icon { padding: 8px; border-radius: var(--radius-sm); }
|
||||
|
||||
@@ -125,7 +94,6 @@ a:hover { color: var(--primary-dark); }
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 24px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: background 0.25s ease, border-color 0.25s ease;
|
||||
}
|
||||
|
||||
/* ── Form Elements ── */
|
||||
@@ -133,12 +101,12 @@ a:hover { color: var(--primary-dark); }
|
||||
.form-label { display: block; font-size: 0.82rem; font-weight: 600; color: var(--text-primary); margin-bottom: 6px; }
|
||||
.form-control {
|
||||
width: 100%; padding: 10px 14px; border-radius: var(--radius-md);
|
||||
background: var(--bg-surface); border: 1px solid var(--border-light);
|
||||
background: #FFFFFF; border: 1px solid var(--border-light);
|
||||
color: var(--text-primary); font-size: 0.9rem; font-family: 'Inter', sans-serif;
|
||||
outline: none; transition: var(--transition); box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.form-control:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15); }
|
||||
.form-control::placeholder { color: var(--text-muted); }
|
||||
.form-control::placeholder { color: #9CA3AF; }
|
||||
select.form-control {
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%236B7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
|
||||
@@ -156,26 +124,20 @@ textarea.form-control { resize: vertical; min-height: 80px; }
|
||||
.badge-warning { background: #FFFBEB; color: #B45309; border: 1px solid #FDE68A; }
|
||||
.badge-info { background: #F0F9FF; color: #0369A1; border: 1px solid #BAE6FD; }
|
||||
|
||||
[data-theme="dark"] .badge-primary { background: rgba(129,140,248,0.15); color: #A5B4FC; border-color: rgba(129,140,248,0.3); }
|
||||
[data-theme="dark"] .badge-success { background: rgba(52,211,153,0.15); color: #6EE7B7; border-color: rgba(52,211,153,0.3); }
|
||||
[data-theme="dark"] .badge-danger { background: rgba(248,113,113,0.15); color: #FCA5A5; border-color: rgba(248,113,113,0.3); }
|
||||
[data-theme="dark"] .badge-warning { background: rgba(252,211,77,0.15); color: #FDE68A; border-color: rgba(252,211,77,0.3); }
|
||||
[data-theme="dark"] .badge-info { background: rgba(56,189,248,0.15); color: #7DD3FC; border-color: rgba(56,189,248,0.3); }
|
||||
|
||||
/* ── Tables ── */
|
||||
.table-wrap { overflow-x: auto; border-radius: var(--radius-lg); border: 1px solid var(--border-light); background: var(--bg-surface); box-shadow: var(--shadow-sm); }
|
||||
.data-table { width: 100%; border-collapse: collapse; font-size: 0.88rem; }
|
||||
.data-table thead th { background: var(--bg-elevated); padding: 14px 16px; text-align: left; font-size: 0.75rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; white-space: nowrap; border-bottom: 1px solid var(--border-light); }
|
||||
.data-table thead th { background: #F9FAFB; padding: 14px 16px; text-align: left; font-size: 0.75rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; white-space: nowrap; border-bottom: 1px solid var(--border-light); }
|
||||
.data-table tbody tr { border-bottom: 1px solid var(--border-light); transition: background 0.15s; }
|
||||
.data-table tbody tr:last-child { border-bottom: none; }
|
||||
.data-table tbody tr:hover { background: var(--bg-elevated); }
|
||||
.data-table tbody tr:hover { background: #F3F4F6; }
|
||||
.data-table td { padding: 14px 16px; color: var(--text-primary); vertical-align: middle; }
|
||||
.data-table .actions { display: flex; gap: 6px; }
|
||||
|
||||
/* ── Modal ── */
|
||||
.modal-overlay {
|
||||
display: none; position: fixed; inset: 0; z-index: 9999;
|
||||
background: rgba(17, 24, 39, 0.5); backdrop-filter: blur(4px);
|
||||
background: rgba(17, 24, 39, 0.4); backdrop-filter: blur(2px);
|
||||
align-items: center; justify-content: center;
|
||||
}
|
||||
.modal-overlay.active { display: flex; }
|
||||
@@ -190,7 +152,7 @@ textarea.form-control { resize: vertical; min-height: 80px; }
|
||||
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
|
||||
.modal-title { font-size: 1.15rem; font-weight: 700; color: var(--text-primary); }
|
||||
.modal-close { background: none; border: none; color: var(--text-muted); cursor: pointer; font-size: 1.4rem; padding: 4px; border-radius: var(--radius-sm); transition: all 0.15s; }
|
||||
.modal-close:hover { background: var(--bg-elevated); color: var(--text-primary); }
|
||||
.modal-close:hover { background: #F3F4F6; color: var(--text-primary); }
|
||||
.modal-footer { display: flex; gap: 12px; justify-content: flex-end; margin-top: 28px; padding-top: 20px; border-top: 1px solid var(--border-light); }
|
||||
|
||||
/* ── Toast ── */
|
||||
@@ -214,7 +176,7 @@ textarea.form-control { resize: vertical; min-height: 80px; }
|
||||
width: min(360px, calc(100vw - 32px));
|
||||
padding: 13px 14px;
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-surface);
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #A7F3D0;
|
||||
border-left: 4px solid var(--accent);
|
||||
border-radius: var(--radius-md);
|
||||
@@ -267,7 +229,7 @@ textarea.form-control { resize: vertical; min-height: 80px; }
|
||||
.page-header h1 { font-size: 1.6rem; font-weight: 700; color: var(--text-primary); letter-spacing: -0.02em; }
|
||||
.page-header p { color: var(--text-secondary); margin-top: 6px; font-size: 0.9rem; }
|
||||
.page-header .breadcrumb { display: flex; gap: 8px; align-items: center; font-size: 0.82rem; color: var(--text-muted); margin-bottom: 12px; }
|
||||
.page-header .breadcrumb .sep { color: var(--border-hover); }
|
||||
.page-header .breadcrumb .sep { color: #D1D5DB; }
|
||||
|
||||
/* ── Utilities ── */
|
||||
.text-muted { color: var(--text-muted); }
|
||||
|
||||
@@ -763,120 +763,3 @@
|
||||
bottom: 66px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ══════════════════════════════
|
||||
DARK THEME OVERRIDES FOR MAPS
|
||||
══════════════════════════════ */
|
||||
[data-theme="dark"] {
|
||||
.map-wrap,
|
||||
#admin-map,
|
||||
#user-map,
|
||||
.leaflet-container {
|
||||
background: #0f172a;
|
||||
}
|
||||
|
||||
.leaflet-control-attribution {
|
||||
background: rgba(15, 23, 42, 0.9) !important;
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
|
||||
.leaflet-control-zoom a,
|
||||
.leaflet-draw-toolbar a {
|
||||
background: var(--bg-surface) !important;
|
||||
color: var(--text-primary) !important;
|
||||
border-color: var(--border-light) !important;
|
||||
}
|
||||
|
||||
.leaflet-control-zoom a:hover,
|
||||
.leaflet-draw-toolbar a:hover {
|
||||
background: var(--bg-elevated) !important;
|
||||
color: var(--primary-light) !important;
|
||||
}
|
||||
|
||||
.map-sidebar {
|
||||
background: rgba(30, 41, 59, 0.95);
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
|
||||
.map-panel-icon {
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
color: var(--primary-light);
|
||||
border-color: rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.layer-item:hover {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
|
||||
.qs-item {
|
||||
background: var(--bg-base);
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
|
||||
.map-analysis {
|
||||
background: rgba(30, 41, 59, 0.95);
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
|
||||
.map-analysis button {
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-secondary);
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
|
||||
.map-analysis button:hover {
|
||||
background: var(--bg-elevated);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.map-analysis button.active {
|
||||
color: #FFFFFF;
|
||||
background: var(--primary-dark);
|
||||
border-color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.spbu-panel {
|
||||
background: rgba(30, 41, 59, 0.95);
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
|
||||
.spbu-result {
|
||||
background: var(--bg-base);
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
|
||||
.spbu-result-distance {
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.leaflet-popup-content-wrapper {
|
||||
background: var(--bg-surface) !important;
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
|
||||
.leaflet-popup-tip {
|
||||
background: var(--bg-surface) !important;
|
||||
}
|
||||
|
||||
.map-legend {
|
||||
background: rgba(30, 41, 59, 0.95);
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
|
||||
.legend-area {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.map-loading {
|
||||
color: var(--text-primary);
|
||||
background: rgba(15, 23, 42, 0.85);
|
||||
}
|
||||
|
||||
.read-only-badge {
|
||||
color: #6EE7B7;
|
||||
background: rgba(52, 211, 153, 0.15);
|
||||
border-color: rgba(52, 211, 153, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -362,55 +362,3 @@ body.admin-page {
|
||||
.admin-main { margin-left: 0; }
|
||||
.stats-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* ══════════════════════════════
|
||||
DARK THEME OVERRIDES FOR PANEL
|
||||
══════════════════════════════ */
|
||||
[data-theme="dark"] {
|
||||
.nav-item:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: var(--primary-glow);
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
background: #090d16;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
background: #111827;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
background: var(--primary-glow);
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.topbar-time {
|
||||
background: var(--bg-elevated);
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
background: var(--bg-surface);
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
|
||||
.map-sidebar-floating {
|
||||
background: var(--bg-surface);
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
|
||||
/* Stat card styles for dark theme */
|
||||
.stat-card.primary .stat-icon { background: rgba(37, 99, 235, 0.15); color: #818CF8; }
|
||||
.stat-card.success .stat-icon { background: rgba(16, 185, 129, 0.15); color: #34D399; }
|
||||
.stat-card.danger .stat-icon { background: rgba(239, 68, 68, 0.15); color: #F87171; }
|
||||
.stat-card.warning .stat-icon { background: rgba(245, 158, 11, 0.15); color: #FCD34D; }
|
||||
.stat-card.info .stat-icon { background: rgba(14, 165, 233, 0.15); color: #38BDF8; }
|
||||
.stat-card.violet .stat-icon { background: rgba(124, 58, 237, 0.15); color: #A78BFA; }
|
||||
}
|
||||
|
||||
|
||||
@@ -87,26 +87,11 @@ function initMap() {
|
||||
|
||||
L.control.zoom({ position: 'bottomright' }).addTo(state.map);
|
||||
|
||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
const tileUrl = isDark
|
||||
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
|
||||
: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png';
|
||||
|
||||
const tileLayer = L.tileLayer(tileUrl, {
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
|
||||
attribution: '© OpenStreetMap, © CARTO',
|
||||
subdomains: 'abcd',
|
||||
maxZoom: 20
|
||||
}).addTo(state.map);
|
||||
|
||||
// Dynamic theme updater for tile layer
|
||||
const observer = new MutationObserver(() => {
|
||||
const dark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
tileLayer.setUrl(dark
|
||||
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
|
||||
: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'
|
||||
);
|
||||
});
|
||||
observer.observe(document.documentElement, { attributes: true });
|
||||
}
|
||||
|
||||
/* ── Fetch GeoJSON ── */
|
||||
@@ -469,28 +454,22 @@ async function findNearestSpbu() {
|
||||
try {
|
||||
const r = await fetch(`${API}/spbu_terdekat.php?lat=${center.lat}&lng=${center.lng}`);
|
||||
const j = await r.json();
|
||||
if (j.status === 'success' && j.data && j.data.features && j.data.features.length > 0) {
|
||||
const feature = j.data.features[0];
|
||||
if (feature && feature.geometry && feature.geometry.coordinates) {
|
||||
const p = feature.properties;
|
||||
const coords = feature.geometry.coordinates;
|
||||
const jarakVal = p.jarak_km !== undefined ? p.jarak_km : (p.jarak_meter / 1000).toFixed(2);
|
||||
const html = `<div class="spbu-result">
|
||||
<div class="spbu-result-name">⛽ ${p.nama}</div>
|
||||
<div class="spbu-result-meta">${p.buka_24_jam ? '🟢 24 Jam' : '🟡 Terbatas'}</div>
|
||||
<div class="spbu-result-distance">${jarakVal} km</div>
|
||||
</div>`;
|
||||
document.getElementById('spbuPanelResult').innerHTML = html;
|
||||
if (j.status === 'success' && j.data) {
|
||||
const p = j.data.properties;
|
||||
const coords = j.data.geometry.coordinates;
|
||||
const html = `<div class="spbu-result">
|
||||
<div class="spbu-result-name">⛽ ${p.nama}</div>
|
||||
<div class="spbu-result-meta">${p.buka_24_jam ? '🟢 24 Jam' : '🟡 Terbatas'}</div>
|
||||
<div class="spbu-result-distance">${p.jarak_km} km</div>
|
||||
</div>`;
|
||||
document.getElementById('spbuPanelResult').innerHTML = html;
|
||||
|
||||
// Show marker
|
||||
if (state.layers.spbuRoute) state.map.removeLayer(state.layers.spbuRoute);
|
||||
state.layers.spbuRoute = L.marker([coords[1], coords[0]], { icon: p.buka_24_jam ? ICONS.spbu24 : ICONS.spbu }).addTo(state.map)
|
||||
.bindPopup(`<b>${p.nama}</b><br>${jarakVal} km dari pusat peta`).openPopup();
|
||||
} else {
|
||||
document.getElementById('spbuPanelResult').innerHTML = `<div class="spbu-result">Data geometri SPBU terdekat tidak valid.</div>`;
|
||||
}
|
||||
// Show marker
|
||||
if (state.layers.spbuRoute) state.map.removeLayer(state.layers.spbuRoute);
|
||||
state.layers.spbuRoute = L.marker([coords[1], coords[0]], { icon: ICONS.spbu }).addTo(state.map)
|
||||
.bindPopup(`<b>${p.nama}</b><br>${p.jarak_km} km dari pusat peta`).openPopup();
|
||||
} else {
|
||||
document.getElementById('spbuPanelResult').innerHTML = `<div class="spbu-result">${j.message || 'Tidak ada SPBU dalam jangkauan'}</div>`;
|
||||
document.getElementById('spbuPanelResult').innerHTML = `<div class="spbu-result">${j.message || 'Tidak ada SPBU'}</div>`;
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('spbuPanelResult').innerHTML = `<div class="spbu-result">Error: ${e.message}</div>`;
|
||||
|
||||
@@ -81,26 +81,11 @@ function initMap() {
|
||||
|
||||
L.control.zoom({ position: 'bottomright' }).addTo(state.map);
|
||||
|
||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
const tileUrl = isDark
|
||||
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
|
||||
: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png';
|
||||
|
||||
const tileLayer = L.tileLayer(tileUrl, {
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
|
||||
attribution: '© OpenStreetMap, © CARTO',
|
||||
subdomains: 'abcd',
|
||||
maxZoom: 20
|
||||
}).addTo(state.map);
|
||||
|
||||
// Dynamic theme updater for tile layer
|
||||
const observer = new MutationObserver(() => {
|
||||
const dark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
tileLayer.setUrl(dark
|
||||
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
|
||||
: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'
|
||||
);
|
||||
});
|
||||
observer.observe(document.documentElement, { attributes: true });
|
||||
}
|
||||
|
||||
async function fetchLayer(path) {
|
||||
@@ -335,25 +320,19 @@ async function findNearestSpbu() {
|
||||
try {
|
||||
const r = await fetch(`${API}/spbu_terdekat.php?lat=${center.lat}&lng=${center.lng}`);
|
||||
const j = await r.json();
|
||||
if (j.status === 'success' && j.data && j.data.features && j.data.features.length > 0) {
|
||||
const feature = j.data.features[0];
|
||||
if (feature && feature.geometry && feature.geometry.coordinates) {
|
||||
const p = feature.properties;
|
||||
const coords = feature.geometry.coordinates;
|
||||
const jarakVal = p.jarak_km !== undefined ? p.jarak_km : (p.jarak_meter / 1000).toFixed(2);
|
||||
document.getElementById('spbuPanelResult').innerHTML = `<div class="spbu-result">
|
||||
<div class="spbu-result-name">⛽ ${p.nama}</div>
|
||||
<div class="spbu-result-meta">${p.buka_24_jam ? '🟢 24 Jam' : '🟡 Terbatas'}</div>
|
||||
<div class="spbu-result-distance">${jarakVal} km</div>
|
||||
</div>`;
|
||||
if (state.layers.spbuRoute) state.map.removeLayer(state.layers.spbuRoute);
|
||||
state.layers.spbuRoute = L.marker([coords[1], coords[0]], { icon: p.buka_24_jam ? ICONS.spbu24 : ICONS.spbu }).addTo(state.map)
|
||||
.bindPopup(`<b>${p.nama}</b><br>${jarakVal} km dari pusat peta`).openPopup();
|
||||
} else {
|
||||
document.getElementById('spbuPanelResult').innerHTML = `<div class="spbu-result">Data geometri SPBU terdekat tidak valid.</div>`;
|
||||
}
|
||||
if (j.status === 'success' && j.data) {
|
||||
const p = j.data.properties;
|
||||
const coords = j.data.geometry.coordinates;
|
||||
document.getElementById('spbuPanelResult').innerHTML = `<div class="spbu-result">
|
||||
<div class="spbu-result-name">⛽ ${p.nama}</div>
|
||||
<div class="spbu-result-meta">${p.buka_24_jam ? '🟢 24 Jam' : '🟡 Terbatas'}</div>
|
||||
<div class="spbu-result-distance">${p.jarak_km} km</div>
|
||||
</div>`;
|
||||
if (state.layers.spbuRoute) state.map.removeLayer(state.layers.spbuRoute);
|
||||
state.layers.spbuRoute = L.marker([coords[1], coords[0]], { icon: ICONS.spbu }).addTo(state.map)
|
||||
.bindPopup(`<b>${p.nama}</b><br>${p.jarak_km} km dari pusat peta`).openPopup();
|
||||
} else {
|
||||
document.getElementById('spbuPanelResult').innerHTML = `<div class="spbu-result">${j.message || 'Tidak ada SPBU dalam jangkauan'}</div>`;
|
||||
document.getElementById('spbuPanelResult').innerHTML = `<div class="spbu-result">${j.message || 'Tidak ada SPBU'}</div>`;
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('spbuPanelResult').innerHTML = `<div class="spbu-result">Error: ${e.message}</div>`;
|
||||
|
||||
Reference in New Issue
Block a user