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 ( +
+
+ + setKepalaKeluarga(e.target.value)} /> +
+
+ + setJumlahAnggota(e.target.value)} /> +
+
+ + +
+ + {parseInt(statusBantuan) === 1 && ( +
+ +
+ {opsiBantuan.map((bantuan) => ( +
+ handleCheckboxChange(bantuan)} + style={{ marginRight: '8px' }} + /> + +
+ ))} +
+
+ )} + +

+ Koordinat Terpilih: {selectedLat?.toFixed(5)}, {selectedLng?.toFixed(5)} +

+ +
+ ); +} + +// 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' && ( +
+
+ setFormJalan({...formJalan, nama: e.target.value})} /> +
+
+ +
+ +
+ )} + + {activeInsertionMode === 'area' && ( +
+
+ setFormArea({...formArea, nama: e.target.value})} /> +
+ +
+ )} + + {activeInsertionMode === 'ibadah' && ( +
+
+ + setFormIbadah({...formIbadah, nama: e.target.value})} /> +
+
+ + +
+
+ + setFormIbadah({...formIbadah, kontak: e.target.value})} /> +
+
+ + setFormIbadah({...formIbadah, radius: parseInt(e.target.value)})} /> +
+ +
+ )} + + {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