diff --git a/App.css b/App.css
new file mode 100644
index 0000000..f90339d
--- /dev/null
+++ b/App.css
@@ -0,0 +1,184 @@
+.counter {
+ font-size: 16px;
+ padding: 5px 10px;
+ border-radius: 5px;
+ color: var(--accent);
+ background: var(--accent-bg);
+ border: 2px solid transparent;
+ transition: border-color 0.3s;
+ margin-bottom: 24px;
+
+ &:hover {
+ border-color: var(--accent-border);
+ }
+ &:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+ }
+}
+
+.hero {
+ position: relative;
+
+ .base,
+ .framework,
+ .vite {
+ inset-inline: 0;
+ margin: 0 auto;
+ }
+
+ .base {
+ width: 170px;
+ position: relative;
+ z-index: 0;
+ }
+
+ .framework,
+ .vite {
+ position: absolute;
+ }
+
+ .framework {
+ z-index: 1;
+ top: 34px;
+ height: 28px;
+ transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
+ scale(1.4);
+ }
+
+ .vite {
+ z-index: 0;
+ top: 107px;
+ height: 26px;
+ width: auto;
+ transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
+ scale(0.8);
+ }
+}
+
+#center {
+ display: flex;
+ flex-direction: column;
+ gap: 25px;
+ place-content: center;
+ place-items: center;
+ flex-grow: 1;
+
+ @media (max-width: 1024px) {
+ padding: 32px 20px 24px;
+ gap: 18px;
+ }
+}
+
+#next-steps {
+ display: flex;
+ border-top: 1px solid var(--border);
+ text-align: left;
+
+ & > div {
+ flex: 1 1 0;
+ padding: 32px;
+ @media (max-width: 1024px) {
+ padding: 24px 20px;
+ }
+ }
+
+ .icon {
+ margin-bottom: 16px;
+ width: 22px;
+ height: 22px;
+ }
+
+ @media (max-width: 1024px) {
+ flex-direction: column;
+ text-align: center;
+ }
+}
+
+#docs {
+ border-right: 1px solid var(--border);
+
+ @media (max-width: 1024px) {
+ border-right: none;
+ border-bottom: 1px solid var(--border);
+ }
+}
+
+#next-steps ul {
+ list-style: none;
+ padding: 0;
+ display: flex;
+ gap: 8px;
+ margin: 32px 0 0;
+
+ .logo {
+ height: 18px;
+ }
+
+ a {
+ color: var(--text-h);
+ font-size: 16px;
+ border-radius: 6px;
+ background: var(--social-bg);
+ display: flex;
+ padding: 6px 12px;
+ align-items: center;
+ gap: 8px;
+ text-decoration: none;
+ transition: box-shadow 0.3s;
+
+ &:hover {
+ box-shadow: var(--shadow);
+ }
+ .button-icon {
+ height: 18px;
+ width: 18px;
+ }
+ }
+
+ @media (max-width: 1024px) {
+ margin-top: 20px;
+ flex-wrap: wrap;
+ justify-content: center;
+
+ li {
+ flex: 1 1 calc(50% - 8px);
+ }
+
+ a {
+ width: 100%;
+ justify-content: center;
+ box-sizing: border-box;
+ }
+ }
+}
+
+#spacer {
+ height: 88px;
+ border-top: 1px solid var(--border);
+ @media (max-width: 1024px) {
+ height: 48px;
+ }
+}
+
+.ticks {
+ position: relative;
+ width: 100%;
+
+ &::before,
+ &::after {
+ content: '';
+ position: absolute;
+ top: -4.5px;
+ border: 5px solid transparent;
+ }
+
+ &::before {
+ left: 0;
+ border-left-color: var(--border);
+ }
+ &::after {
+ right: 0;
+ border-right-color: var(--border);
+ }
+}
diff --git a/App.jsx b/App.jsx
new file mode 100644
index 0000000..63a518f
--- /dev/null
+++ b/App.jsx
@@ -0,0 +1,574 @@
+import React, { useState, useEffect } from 'react';
+import { MapContainer, TileLayer, Marker, Popup, Circle, Polyline, Polygon, useMapEvents, LayersControl, LayerGroup, useMap } from 'react-leaflet';
+import L from 'leaflet';
+import 'leaflet/dist/leaflet.css';
+import markerIconPng from "leaflet/dist/images/marker-icon.png";
+import markerShadowPng from "leaflet/dist/images/marker-shadow.png";
+
+// Pengaturan Default Marker Objek
+const DefaultIcon = L.icon({
+ iconUrl: markerIconPng,
+ shadowUrl: markerShadowPng,
+ iconSize: [25, 41],
+ iconAnchor: [12, 41],
+ popupAnchor: [1, -34],
+});
+L.Marker.prototype.options.icon = DefaultIcon;
+
+// Pengaturan Marker Kustom Penduduk Miskin
+const MiskinIcon = L.divIcon({
+ className: 'custom-miskin-marker',
+ html: `
`,
+ iconSize: [12, 12],
+ iconAnchor: [6, 6]
+});
+
+// Pengaturan Marker Kustom Rumah Ibadah
+const IbadahIcon = L.icon({
+ iconUrl: markerIconPng,
+ iconSize: [25, 41],
+ iconAnchor: [12, 41],
+ popupAnchor: [1, -34],
+});
+
+// Fungsi Parser Geometri GeoJSON dari PostGIS
+const parsePoint = (geom) => [geom.coordinates[1], geom.coordinates[0]];
+const parseLineString = (geom) => geom.coordinates.map(coord => [coord[1], coord[0]]);
+const parsePolygon = (geom) => geom.coordinates.map(ring => ring.map(coord => [coord[1], coord[0]]));
+
+// Fungsi Decoder Polyline OSRM Manual (Pastikan ini ada agar tidak undefined)
+const decodeOSRMPoints = (str, precision) => {
+ var index = 0, lat = 0, lng = 0, coordinates = [], shift = 0, result = 0, byte = null, lat_change, lng_change, factor = Math.pow(10, precision || 5);
+ while (index < str.length) {
+ byte = null; shift = 0; result = 0;
+ do { byte = str.charCodeAt(index++) - 63; result |= (byte & 0x1f) << shift; shift += 5; } while (byte >= 0x20);
+ lat_change = ((result & 1) ? ~(result >> 1) : (result >> 1)); lat += lat_change;
+ byte = null; shift = 0; result = 0;
+ do { byte = str.charCodeAt(index++) - 63; result |= (byte & 0x1f) << shift; shift += 5; } while (byte >= 0x20);
+ lng_change = ((result & 1) ? ~(result >> 1) : (result >> 1)); lng += lng_change;
+ coordinates.push([lat / factor, lng / factor]);
+ }
+ return coordinates;
+};
+
+// Listener Klik Peta Universal
+function MapClickHandler({ activeMode, onMapClick }) {
+ useMapEvents({
+ click(e) {
+ if (activeMode) {
+ onMapClick(e.latlng.lat, e.latlng.lng);
+ }
+ },
+ });
+ return null;
+}
+
+// βοΈ MENAMBAHKAN DEKLARASI MAP VIEW UPDATER AGAR TIDAK ERROR REFERENCE
+function MapViewUpdater({ center }) {
+ const map = useMap();
+ useEffect(() => {
+ if (center) {
+ map.setView(center, map.getZoom());
+ }
+ }, [center, map]);
+ return null;
+}
+
+// Sub-Komponen Form Penduduk Miskin (Fokus hanya pada Input Formulir)
+function PendudukMiskinForm({ selectedLat, selectedLng, onSaveSuccess, onClose }) {
+ const [kepalaKeluarga, setKepalaKeluarga] = useState('');
+ const [jumlahAnggota, setJumlahAnggota] = useState(1);
+ const [statusBantuan, setStatusBantuan] = useState(0);
+ const [jenisBantuan, setJenisBantuan] = useState([]);
+
+ const opsiBantuan = [
+ 'BPJS_Kesehatan', 'PKH', 'PIP/KIP', 'Bansos_Tunai', 'BPNT/Sembako', 'Santunan Anak Yatim-Piatu'
+ ];
+
+ const handleCheckboxChange = (bantuan) => {
+ if (jenisBantuan.includes(bantuan)) {
+ setJenisBantuan(jenisBantuan.filter((item) => item !== bantuan));
+ } else {
+ setJenisBantuan([...jenisBantuan, bantuan]);
+ }
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ const payload = {
+ kepala_keluarga: kepalaKeluarga,
+ jumlah_anggota: parseInt(jumlahAnggota),
+ status_bantuan: parseInt(statusBantuan),
+ jenis_bantuan: jenisBantuan,
+ lat: selectedLat,
+ lng: selectedLng
+ };
+
+ try {
+ const response = await fetch('http://localhost:5000/api/miskin/create', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload)
+ });
+ const result = await response.json();
+ if (result.status === 'success') {
+ alert(`Data Berhasil Disimpan!\nID Ibadah Terdekat: ${result.closest_hub}`);
+ onSaveSuccess();
+ onClose();
+ } else {
+ alert(`Gagal menyimpan data: ${result.message}`);
+ }
+ } catch (error) {
+ console.error('Error fetching data:', error);
+ alert('Terjadi kesalahan jaringan saat menyimpan data.');
+ }
+ };
+
+ return (
+
+ );
+}
+
+// Komponen Legenda Spasial
+function MapLegend() {
+ const map = useMap();
+ useEffect(() => {
+ const legend = L.control({ position: 'bottomright' });
+ legend.onAdd = () => {
+ const div = L.DomUtil.create('div', 'info legend');
+ div.style.background = 'white'; div.style.padding = '12px'; div.style.borderRadius = '8px';
+ div.style.boxShadow = '0 2px 6px rgba(0,0,0,0.3)'; div.style.fontSize = '13px'; div.style.lineHeight = '20px'; div.style.color = '#333';
+ div.innerHTML = `
+ Legenda Peta
+ Tempat Ibadah:
Radius Pelayanan (300m+)
+ Kesejahteraan:
Penduduk Miskin
+ Jaringan Jalan:
Jln Nasional
Jln Provinsi
Jln Kabupaten
+ Batas Sektoral:
Wilayah / Area Sektor
`;
+ return div;
+ };
+ legend.addTo(map);
+ return () => { legend.remove(); };
+ }, [map]);
+ return null;
+}
+
+const getIbadahEmoji = (jenis) => {
+ if (!jenis) return 'π'; // Jaga-jaga jika nilainya null/undefined
+ switch(jenis.toLowerCase()) {
+ case 'masjid': return 'π';
+ case 'gereja': return 'βͺ';
+ case 'vihara': return 'π';
+ case 'klenteng': return 'β©οΈ';
+ case 'pura': return 'π';
+ default: return 'π';
+ }
+};
+
+// Pengaturan Style Warna Garis Jalan Berdasarkan Klasifikasi Database
+const getJalanStyle = (status) => {
+ switch(status) {
+ case 'Jln Nasional': return { color: '#0000ff', weight: 5 };
+ case 'Jln Provinsi': return { color: '#008000', weight: 4 };
+ default: return { color: '#ffff00', weight: 3 };
+ }
+};
+
+function App() {
+ const pontianakCenter = [-0.0263, 109.3425];
+
+ const [dataJalan, setDataJalan] = useState([]);
+ const [dataArea, setDataArea] = useState([]);
+ const [dataIbadah, setDataIbadah] = useState([]);
+ const [dataMiskin, setDataMiskin] = useState([]);
+ const [isRoutingLoading, setIsRoutingLoading] = useState(false);
+ const [routingCoords, setRoutingCoords] = useState([]);
+
+ const [activeInsertionMode, setActiveInsertionMode] = useState(null);
+ const [drawCoords, setDrawCoords] = useState([]);
+ const [showModal, setShowModal] = useState(false);
+
+ const [formJalan, setFormJalan] = useState({ nama: '', status: 'Jln Kabupaten' });
+ const [formArea, setFormArea] = useState({ nama: '' });
+ const [formIbadah, setFormIbadah] = useState({ nama: '', jenis: 'masjid', kontak: '', radius: 300 });
+
+ const loadAllData = () => {
+ fetch('http://localhost:5000/api/jalan').then(res => res.json()).then(data => setDataJalan(data));
+ fetch('http://localhost:5000/api/area').then(res => res.json()).then(data => setDataArea(data));
+ fetch('http://localhost:5000/api/ibadah').then(res => res.json()).then(data => setDataIbadah(data));
+ fetch('http://localhost:5000/api/miskin').then(res => res.json()).then(data => setDataMiskin(data));
+ };
+
+ useEffect(() => { loadAllData(); }, []);
+
+ // Fungsi Routing Network Terpusat (Dikendalikan oleh Komponen App Utama)
+ const handleFetchRoute = async (miskinLat, miskinLng, idIbadahTerdekat) => {
+ setIsRoutingLoading(true);
+ const ibadahTerdekat = dataIbadah.find(i => i.id === idIbadahTerdekat);
+
+ if (!ibadahTerdekat || !ibadahTerdekat.geom) {
+ alert("Maaf, data rincian koordinat Rumah Ibadah terdekat tidak ditemukan.");
+ setIsRoutingLoading(false);
+ return;
+ }
+
+ const [ibadahLng, ibadahLat] = ibadahTerdekat.geom.coordinates;
+
+ try {
+ const url = `https://router.project-osrm.org/route/v1/driving/${ibadahLng},${ibadahLat};${miskinLng},${miskinLat}?overview=full&geometries=polyline`;
+ const response = await fetch(url);
+ const data = await response.json();
+
+ if (data.code === 'Ok' && data.routes.length > 0) {
+ const encodedRoute = data.routes[0].geometry;
+ const decodedCoords = decodeOSRMPoints(encodedRoute);
+ setRoutingCoords(decodedCoords);
+ } else {
+ alert("Gagal memetakan rute jalan resmi.");
+ }
+ } catch (error) {
+ console.error(error);
+ alert("Koneksi rute OSRM terputus.");
+ } finally {
+ setIsRoutingLoading(false);
+ }
+ };
+
+ const handleToolbarToggle = (mode) => {
+ setActiveInsertionMode(activeInsertionMode === mode ? null : mode);
+ setDrawCoords([]);
+ };
+
+ const handleMapClick = (lat, lng) => {
+ if (activeInsertionMode === 'ibadah' || activeInsertionMode === 'miskin') {
+ setDrawCoords([[lat, lng]]);
+ setShowModal(true);
+ } else if (activeInsertionMode === 'jalan' || activeInsertionMode === 'area') {
+ setDrawCoords([...drawCoords, [lat, lng]]);
+ }
+ };
+
+ const closeModal = () => {
+ setShowModal(false);
+ setDrawCoords([]);
+ setActiveInsertionMode(null);
+ setFormJalan({ nama: '', status: 'Jln Kabupaten' });
+ setFormArea({ nama: '' });
+ setFormIbadah({ nama: '', jenis: 'masjid', kontak: '', radius: 300 });
+ };
+
+ const submitJalanForm = (e) => {
+ e.preventDefault();
+ fetch('http://localhost:5000/api/jalan/create', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ ...formJalan, coords: drawCoords })
+ })
+ .then(res => res.json())
+ .then((result) => {
+ if (result.status === 'success') {
+ alert("Jalan Berhasil Disimpan!"); loadAllData(); closeModal();
+ } else { alert("Gagal menyimpan jalan: " + result.message); }
+ }).catch(err => alert("Terjadi kesalahan jaringan."));
+ };
+
+ const submitAreaForm = (e) => {
+ e.preventDefault();
+ fetch('http://localhost:5000/api/area/create', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ ...formArea, coords: drawCoords })
+ })
+ .then(res => res.json())
+ .then((result) => {
+ if (result.status === 'success') {
+ alert("Area Berhasil Disimpan!"); loadAllData(); closeModal();
+ } else { alert("Gagal menyimpan area: " + result.message); }
+ }).catch(err => alert("Terjadi kesalahan jaringan."));
+ };
+
+ const submitIbadahForm = (e) => {
+ e.preventDefault();
+ fetch('http://localhost:5000/api/ibadah/create', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ ...formIbadah, lat: drawCoords[0][0], lng: drawCoords[0][1] })
+ })
+ .then(res => res.json())
+ .then((result) => {
+ if (result.status === 'success') {
+ alert("Rumah Ibadah Berhasil Disimpan!"); loadAllData(); closeModal();
+ } else { alert("Gagal menyimpan rumah ibadah: " + result.message); }
+ }).catch(err => alert("Terjadi kesalahan jaringan."));
+ };
+
+ return (
+
+
+ {/* Action Toolbar */}
+
+
+
+
+
+
+ {(activeInsertionMode === 'jalan' || activeInsertionMode === 'area') && drawCoords.length > 1 && (
+
+ )}
+
+
+ {/* MAP CONTAINER UTAMA */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* 1. LAYER GROUP UNTUK JALAN */}
+
+
+ {dataJalan.map(j => {
+ if (!j.geom) return null;
+ const pathOptions = getJalanStyle(j.status_jalan);
+ return (
+
+ Nama Jalan: {j.nama_jalan || 'Tanpa Nama'}
Status: {j.status_jalan}
+
+ );
+ })}
+
+
+
+ {/* 2. LAYER GROUP UNTUK AREA BANJIR */}
+
+
+ {dataArea.map(a => {
+ if (!a.geom) return null;
+ return (
+
+ Area Wilayah Sektor: {a.nama_area || 'Sektor'}
+
+ );
+ })}
+
+
+
+ {/* 3. LAYER GROUP UNTUK RUMAH IBADAH (DENGAN RADIUS DINAMIS) */}
+
+
+ {dataIbadah.map(i => {
+ if (!i.geom) return null;
+ const [ibadahLat, ibadahLng] = parsePoint(i.geom);
+ return (
+
+
+ {/* π΅ PERBAIKAN: Menggunakan i.radius sesuai properti dari database */}
+
+
+ {/* Marker Rumah Ibadah */}
+
+
+ {getIbadahEmoji(i.jenis_ibadah || 'lainnya')} {i.nama_ibadah}
+ Jenis: {(i.jenis_ibadah?.toUpperCase()) || 'LAINNYA'}
+ Kontak: {i.kontak || '-'}
+ {/* Sesuai dengan properti i.radius */}
+ Radius Aksi: {i.radius || 0} meter
+
+
+
+
+ );
+ })}
+
+
+
+ {/* 4. LOOPING DATA PENDUDUK MISKIN YANG BENAR (Di dalam Peta) */}
+
+
+ {dataMiskin.map(m => {
+ if (!m.geom) return null;
+ const [miskinLat, miskinLng] = parsePoint(m.geom);
+ return (
+
+
+
+
π€ Kepala Keluarga: {m.kepala_keluarga}
+
π₯ Jumlah Anggota: {m.jumlah_anggota} Jiwa
+
π¦ Bantuan: {m.jenis_bantuan && m.jenis_bantuan.length > 0 ? m.jenis_bantuan.join(', ') : 'Belum menerima'}
+
π Hub Terdekat ID: {m.id_ibadah_terdekat || 'Kosong'}
+
+
+
+
+
+ );
+ })}
+
+
+
+
+ {/* RENDERING JALUR RUTE NYATA */}
+ {routingCoords.length > 0 && (
+
+ )}
+
+ {/* CONTROLLER EVENT MAP */}
+
+
+
+
+
+
+ {/* Modal Input Overlay */}
+ {showModal && (
+
+
+
+
Tambah Data {activeInsertionMode?.toUpperCase()}
+
+
+
+ {activeInsertionMode === 'jalan' && (
+
+ )}
+
+ {activeInsertionMode === 'area' && (
+
+ )}
+
+ {activeInsertionMode === 'ibadah' && (
+
+ )}
+
+ {activeInsertionMode === 'miskin' && drawCoords.length > 0 && (
+
{ loadAllData(); closeModal(); }}
+ />
+ )}
+
+
+ )}
+
+ );
+}
+
+export default App;
\ No newline at end of file
diff --git a/hero.png b/hero.png
new file mode 100644
index 0000000..02251f4
Binary files /dev/null and b/hero.png differ
diff --git a/index.css b/index.css
new file mode 100644
index 0000000..c264126
--- /dev/null
+++ b/index.css
@@ -0,0 +1,332 @@
+:root {
+ --text: #6b6375;
+ --text-h: #08060d;
+ --bg: #fff;
+ --border: #e5e4e7;
+ --code-bg: #f4f3ec;
+ --accent: #aa3bff;
+ --accent-bg: rgba(170, 59, 255, 0.1);
+ --accent-border: rgba(170, 59, 255, 0.5);
+ --social-bg: rgba(244, 243, 236, 0.5);
+ --shadow:
+ rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
+
+ --sans: system-ui, 'Segoe UI', Roboto, sans-serif;
+ --heading: system-ui, 'Segoe UI', Roboto, sans-serif;
+ --mono: ui-monospace, Consolas, monospace;
+
+ font: 18px/145% var(--sans);
+ letter-spacing: 0.18px;
+ color-scheme: light dark;
+ color: var(--text);
+ background: var(--bg);
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ @media (max-width: 1024px) {
+ font-size: 16px;
+ }
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --text: #9ca3af;
+ --text-h: #f3f4f6;
+ --bg: #16171d;
+ --border: #2e303a;
+ --code-bg: #1f2028;
+ --accent: #c084fc;
+ --accent-bg: rgba(192, 132, 252, 0.15);
+ --accent-border: rgba(192, 132, 252, 0.5);
+ --social-bg: rgba(47, 48, 58, 0.5);
+ --shadow:
+ rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
+ }
+
+ #social .button-icon {
+ filter: invert(1) brightness(2);
+ }
+}
+
+body {
+ margin: 0;
+ padding: 0;
+}
+
+#root {
+ width: 100vw;
+ height: 100vh;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ box-sizing: border-box;
+}
+
+h1,
+h2 {
+ font-family: var(--heading);
+ font-weight: 500;
+ color: var(--text-h);
+}
+
+h1 {
+ font-size: 56px;
+ letter-spacing: -1.68px;
+ margin: 32px 0;
+ @media (max-width: 1024px) {
+ font-size: 36px;
+ margin: 20px 0;
+ }
+}
+h2 {
+ font-size: 24px;
+ line-height: 118%;
+ letter-spacing: -0.24px;
+ margin: 0 0 8px;
+ @media (max-width: 1024px) {
+ font-size: 20px;
+ }
+}
+p {
+ margin: 0;
+}
+
+code,
+.counter {
+ font-family: var(--mono);
+ display: inline-flex;
+ border-radius: 4px;
+ color: var(--text-h);
+}
+
+code {
+ font-size: 15px;
+ line-height: 135%;
+ padding: 4px 8px;
+ background: var(--code-bg);
+}
+
+/* Custom Floating Map Legend styling */
+.map-legend {
+ position: absolute;
+ bottom: 30px;
+ right: 20px;
+ background: white;
+ padding: 12px 16px;
+ border-radius: 4px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ font-family: system-ui, -apple-system, sans-serif;
+ font-size: 14px;
+ color: #333;
+ z-index: 1000; /* Keeps it layered above the Leaflet tiles canvas */
+ min-width: 150px;
+}
+
+.legend-section {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.legend-item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.legend-label {
+ font-weight: 500;
+}
+
+.legend-divider {
+ border: 0;
+ border-top: 1px solid #ccc;
+ margin: 10px 0;
+}
+
+/* Icon Representation Geometries */
+.legend-icon {
+ font-size: 16px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 16px;
+ height: 16px;
+}
+
+.circle-orange {
+ width: 12px;
+ height: 12px;
+ background-color: #ff9f00;
+ border-radius: 50%;
+ margin-left: 2px;
+}
+
+/* Line Assets styling */
+.legend-line {
+ width: 20px;
+ height: 4px;
+ border-radius: 2px;
+ display: inline-block;
+}
+
+.line-blue { background-color: #0000ff; }
+.line-green { background-color: #008000; }
+.line-yellow { background-color: #ffff00; }
+
+/* Polygon Box Assets styling */
+.legend-box {
+ width: 18px;
+ height: 14px;
+ display: inline-block;
+ border-radius: 2px;
+}
+
+.box-purple {
+ background-color: rgba(128, 0, 128, 0.4);
+ border: 2px solid purple;
+}
+
+/* Floating Pill-shaped Menu Bar styling */
+.map-action-bar {
+ position: absolute;
+ top: 12px;
+ left: 60px; /* Shifts it right over so it clears the map zoom panel */
+ background: white;
+ padding: 6px 14px;
+ border-radius: 30px; /* Soft round pill border design edge */
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
+ font-family: system-ui, -apple-system, sans-serif;
+ z-index: 1000; /* Pushes element visibility above standard base map layer */
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+.action-btn {
+ background: transparent;
+ border: none;
+ font-size: 14px;
+ font-weight: 600;
+ color: #1a1a1a;
+ cursor: pointer;
+ padding: 6px 10px;
+ border-radius: 20px;
+ transition: all 0.2s ease;
+}
+
+/* Hover style template to test structural responsiveness */
+.action-btn:hover {
+ background-color: #f3f4f6;
+ color: #000;
+}
+
+.action-btn.active {
+ background-color: #e0e7ff;
+ color: #4f46e5;
+ border: 1px solid #c7d2fe;
+}
+
+.font-medium {
+ font-weight: 600;
+}
+
+/* Specific formatting adjustments for the Location marker */
+.location-btn {
+ color: #0026e6;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.location-btn:hover {
+ background-color: rgba(0, 38, 230, 0.08);
+}
+
+.pin-emoji {
+ font-size: 14px;
+}
+
+/* Insertion Toolbar Highlight Styling */
+.action-btn.btn-insert.inserting {
+ background-color: #f59e0b !important;
+ color: white !important;
+ border-color: #d97706 !important;
+ font-weight: bold;
+}
+
+/* Modal Overlay Framework wrapper */
+.custom-modal-overlay {
+ position: absolute;
+ top: 0; left: 0; right: 0; bottom: 0;
+ background-color: rgba(0, 0, 0, 0.5);
+ display: flex; justify-content: center; align-items: center;
+ z-index: 9999; /* Higher than Leaflet overlays */
+}
+
+.custom-modal-container {
+ background: white;
+ padding: 24px;
+ border-radius: 12px;
+ width: 450px;
+ max-height: 85vh;
+ overflow-y: auto;
+ box-shadow: 0 10px 25px rgba(0,0,0,0.2);
+ font-family: 'Segoe UI', Roboto, sans-serif;
+}
+
+.modal-header {
+ display: flex; justify-content: space-between; align-items: center;
+ margin-bottom: 12px;
+}
+.modal-header h2 { margin: 0; font-size: 1.4rem; color: #1f2937; }
+.close-x-btn { background: none; border: none; font-size: 24px; cursor: pointer; color: #9ca3af; }
+.close-x-btn:hover { color: #111827; }
+
+.coord-badge {
+ background-color: #f3f4f6; color: #4b5563;
+ padding: 8px 12px; border-radius: 6px;
+ font-size: 0.85rem; font-family: monospace; margin-bottom: 16px;
+}
+
+.form-group { margin-bottom: 14px; display: flex; flex-direction: column; }
+.form-group label { font-size: 0.85rem; font-weight: 600; margin-bottom: 5px; color: #4b5563; }
+.form-group input, .form-group select {
+ padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.9rem;
+}
+.form-group input:focus, .form-group select:focus {
+ outline: none; border-color: #4f46e5; box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.1);
+}
+
+/* Checkbox alignment metrics */
+.checkbox-grid {
+ display: grid; grid-template-columns: 1fr; gap: 6px;
+ background: #f9fafb; padding: 10px; border-radius: 6px; border: 1px solid #e5e7eb;
+}
+.check-item { display: flex; align-items: center; gap: 8px; font-size: 0.85rem; cursor: pointer; }
+
+.submit-btn {
+ width: 100%; padding: 10px; border: none; border-radius: 6px;
+ font-weight: 600; font-size: 1rem; cursor: pointer; transition: background 0.2s; color: white;
+}
+.submit-btn.save-ibadah { background-color: #2563eb; }
+.submit-btn.save-ibadah:hover { background-color: #1d4ed8; }
+.submit-btn.save-miskin { background-color: #dc2626; }
+.submit-btn.save-miskin:hover { background-color: #b91c1c; }
+
+.finish-draw-btn {
+ background-color: #10b981;
+ color: white;
+ border: 1px solid #059669;
+ padding: 8px 14px;
+ border-radius: 6px;
+ font-weight: bold;
+ cursor: pointer;
+ animation: pulse 2s infinite;
+}
+.finish-draw-btn:hover {
+ background-color: #059669;
+}
\ No newline at end of file
diff --git a/main.jsx b/main.jsx
new file mode 100644
index 0000000..ce6ae98
--- /dev/null
+++ b/main.jsx
@@ -0,0 +1,13 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App.jsx'
+import './index.css'
+
+// Add this line right here!
+import 'leaflet/dist/leaflet.css'
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+ ,
+)
\ No newline at end of file