diff --git a/admin.html b/admin.html index 098fe8e..52617c3 100644 --- a/admin.html +++ b/admin.html @@ -1099,6 +1099,35 @@ } } + /* ---- Custom Marker ---- */ + .custom-marker { + display: flex; justify-content: center; align-items: center; + } + .marker-halo { + position: absolute; + width: 40px; height: 40px; + border-radius: 50%; + animation: pulse 2s infinite ease-out; + } + .marker-halo.spbu { background: rgba(34, 197, 94, 0.5); } + .marker-halo.spbu-off { background: rgba(245, 158, 11, 0.5); } + .marker-halo.ibadah { background: rgba(14, 165, 233, 0.5); } + .marker-halo.miskin { background: rgba(239, 68, 68, 0.5); } + + .marker-icon-inner { + position: relative; z-index: 2; + width: 28px; height: 28px; + background: white; border-radius: 50%; + box-shadow: 0 2px 8px rgba(0,0,0,0.5); + display: flex; justify-content: center; align-items: center; + font-size: 14px; + } + + @keyframes pulse { + 0% { transform: scale(0.5); opacity: 1; } + 100% { transform: scale(1.5); opacity: 0; } + } + /* ---- Floating Panels ---- */ .search-panel { position: absolute; top: 16px; left: 50%; transform: translateX(-50%); diff --git a/js/app.js b/js/app.js index 3a68875..4f06d42 100644 --- a/js/app.js +++ b/js/app.js @@ -989,15 +989,16 @@ function renderMapLayers() { // Render SPBU window.appState.data.spbu.forEach(item => { - const color = item.status === '24jam' ? '#22c55e' : '#f59e0b'; - const marker = L.circleMarker([item.latitude, item.longitude], { - radius: 6, - fillColor: color, - color: '#fff', - weight: 2, - opacity: 1, - fillOpacity: 0.8 - }).bindPopup(`${item.nama}
Status: ${item.status === '24jam' ? '24 Jam' : 'Tidak 24 Jam'}`); + const is24 = item.status === '24jam'; + const haloClass = is24 ? 'spbu' : 'spbu-off'; + const customIcon = L.divIcon({ + className: 'custom-marker', + html: `
`, + iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -14] + }); + + const marker = L.marker([item.latitude, item.longitude], { icon: customIcon }) + .bindPopup(`${item.nama}
Status: ${is24 ? '24 Jam' : 'Tidak 24 Jam'}`); const layer = item.status === '24jam' ? window.appState.layers.spbu24 : window.appState.layers.spbuTidak; marker.addTo(layer); @@ -1036,27 +1037,25 @@ function renderMapLayers() { // Render Ibadah window.appState.data.ibadah.forEach(item => { - const marker = L.circleMarker([item.latitude, item.longitude], { - radius: 5, - fillColor: '#8b5cf6', - color: '#fff', - weight: 2, - opacity: 1, - fillOpacity: 0.8 - }).bindPopup(`${item.nama}
${item.alamat}`); + const ibadahIcon = L.divIcon({ + className: 'custom-marker', + html: `
🕌
`, + iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -14] + }); + const marker = L.marker([item.latitude, item.longitude], { icon: ibadahIcon }) + .bindPopup(`${item.nama}
${item.alamat}`); marker.addTo(window.appState.layers.ibadah); }); // Render Miskin window.appState.data.miskin.forEach(item => { - const marker = L.circleMarker([item.latitude, item.longitude], { - radius: 5, - fillColor: '#000000', - color: '#fff', - weight: 2, - opacity: 1, - fillOpacity: 0.8 - }).bindPopup(`${item.nama_kk}
${item.keterangan || ''}`); + const miskinIcon = L.divIcon({ + className: 'custom-marker', + html: `
🏠
`, + iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -14] + }); + const marker = L.marker([item.latitude, item.longitude], { icon: miskinIcon }) + .bindPopup(`${item.nama_kk}
${item.keterangan || ''}`); marker.addTo(window.appState.layers.miskin); }); } diff --git a/poverty.html b/poverty.html index b8ec6d0..6bd6d95 100644 --- a/poverty.html +++ b/poverty.html @@ -145,6 +145,33 @@ #loader { position: fixed; inset: 0; background: var(--clr-bg); color: var(--clr-text); display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 9999; gap: 12px; } .loader-spinner { width: 36px; height: 36px; border: 3px solid var(--clr-border); border-top-color: var(--clr-danger); border-radius: 50%; animation: spin 0.8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } + /* ---- Custom Marker ---- */ + .custom-marker { + display: flex; justify-content: center; align-items: center; + } + .marker-halo { + position: absolute; + width: 40px; height: 40px; + border-radius: 50%; + animation: pulse 2s infinite ease-out; + } + .marker-halo.ibadah { background: rgba(34, 197, 94, 0.5); } + .marker-halo.miskin-in { background: rgba(239, 68, 68, 0.5); } + .marker-halo.miskin-out { background: rgba(30, 41, 59, 0.5); } + + .marker-icon-inner { + position: relative; z-index: 2; + width: 28px; height: 28px; + background: white; border-radius: 50%; + box-shadow: 0 2px 8px rgba(0,0,0,0.5); + display: flex; justify-content: center; align-items: center; + font-size: 14px; + } + + @keyframes pulse { + 0% { transform: scale(0.5); opacity: 1; } + 100% { transform: scale(1.5); opacity: 0; } + } @@ -274,9 +301,23 @@ ibadahLayer = L.layerGroup().addTo(map); miskinLayer = L.layerGroup().addTo(map); - ibadahIcon = L.icon({ iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-green.png', shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png', iconSize: [25,41], iconAnchor: [12,41], popupAnchor: [1,-34], shadowSize: [41,41] }); - miskinRedIcon = L.icon({ iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png', shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png', iconSize: [25,41], iconAnchor: [12,41], popupAnchor: [1,-34], shadowSize: [41,41] }); - miskinBlackIcon = L.icon({ iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-black.png', shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png', iconSize: [25,41], iconAnchor: [12,41], popupAnchor: [1,-34], shadowSize: [41,41] }); + const ibadahCustomIcon = L.divIcon({ + className: 'custom-marker', + html: `
🕌
`, + iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -14] + }); + + const miskinInIcon = L.divIcon({ + className: 'custom-marker', + html: `
🏠
`, + iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -14] + }); + + const miskinOutIcon = L.divIcon({ + className: 'custom-marker', + html: `
🏠
`, + iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -14] + }); try { const [ir, mr] = await Promise.all([fetch('backend/read_ibadah.php'), fetch('backend/read_miskin.php')]); @@ -289,8 +330,6 @@ window.updateRadius = function(id, val) { ibadahRadiuses[id] = parseInt(val); - const el = document.getElementById('radius-val-' + id); - if (el) el.textContent = val; renderBuffers(); renderMiskin(); }; @@ -314,18 +353,23 @@ ibadahLayer.clearLayers(); ibadahPoints = []; const bounds = []; const f = document.getElementById('search-ibadah').value.toLowerCase(); - ibadahData.forEach(item => { - if (f && !item.nama.toLowerCase().includes(f)) return; - const lat = parseFloat(item.latitude), lng = parseFloat(item.longitude); + ibadahData.forEach(i => { + if (f && !i.nama.toLowerCase().includes(f)) return; + const lat = parseFloat(i.latitude), lng = parseFloat(i.longitude); if (isNaN(lat) || isNaN(lng)) return; - ibadahPoints.push({ id: item.id, pt: turf.point([lng, lat]) }); - const r = ibadahRadiuses[item.id] || 0; - const popup = '' + item.nama + '
' + item.alamat + '' - + ''; - L.marker([lat, lng], { icon: ibadahIcon }).bindPopup(popup, { minWidth: 220 }).addTo(ibadahLayer); + const rVal = ibadahRadiuses[i.id] || 0; + ibadahPoints.push({ id: i.id, geom: [lng, lat], rad: rVal }); + + const marker = L.marker([lat, lng], { icon: L.divIcon({ className: 'custom-marker', html: `
🕌
`, iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -14] }) }) + .bindPopup(` + ${i.nama}
${i.alamat}

+ + +
${rVal}
+ `) + .addTo(ibadahLayer); bounds.push([lat, lng]); }); @@ -339,18 +383,24 @@ miskinLayer.clearLayers(); const bounds = []; const f = document.getElementById('search-miskin').value.toLowerCase(); - miskinData.forEach(item => { - if (f && !item.nama_kk.toLowerCase().includes(f)) return; - const lat = parseFloat(item.latitude), lng = parseFloat(item.longitude); + miskinData.forEach(m => { + if (f && !m.nama_kk.toLowerCase().includes(f)) return; + const lat = parseFloat(m.latitude), lng = parseFloat(m.longitude); if (isNaN(lat) || isNaN(lng)) return; - const pt = turf.point([lng, lat]); + const pt = [lng, lat]; let inRadius = false; - for (const ib of ibadahPoints) { - const km = ibadahRadiuses[ib.id] / 1000; - if (km > 0 && turf.distance(pt, ib.pt, { units: 'kilometers' }) <= km) { inRadius = true; break; } - } - L.marker([lat, lng], { icon: inRadius ? miskinRedIcon : miskinBlackIcon }) - .bindPopup('KK: ' + item.nama_kk + '
' + (item.keterangan || '-') + '
Status: ' + (inRadius ? '🔴 Dalam Radius' : '⚫ Luar Radius')) + ibadahPoints.forEach(ib => { + if (ib.rad > 0 && turf.distance(turf.point(ib.geom), turf.point(pt), { units: 'meters' }) <= ib.rad) inRadius = true; + }); + + const icon = L.divIcon({ + className: 'custom-marker', + html: `
🏠
`, + iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -14] + }); + + L.marker([lat, lng], { icon: icon }) + .bindPopup('KK: ' + m.nama_kk + '
' + (m.keterangan || '-') + '
Status: ' + (inRadius ? '🔴 Dalam Radius' : '⚫ Luar Radius')) .addTo(miskinLayer); bounds.push([lat, lng]); }); diff --git a/spbu.html b/spbu.html index c795348..a7dd760 100644 --- a/spbu.html +++ b/spbu.html @@ -147,6 +147,34 @@ .legend-panel h3 { font-size: 11px; font-weight: 700; color: var(--clr-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 10px; } .legend-row { display: flex; align-items: center; gap: 10px; margin-bottom: 7px; color: var(--clr-text); font-size: 12px; } .legend-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; } + + /* ---- Custom Marker ---- */ + .custom-marker { + display: flex; justify-content: center; align-items: center; + } + .marker-halo { + position: absolute; + width: 40px; height: 40px; + border-radius: 50%; + animation: pulse 2s infinite ease-out; + } + .marker-halo.spbu { background: rgba(34, 197, 94, 0.5); } + .marker-halo.spbu-off { background: rgba(245, 158, 11, 0.5); } + .marker-halo.user { background: rgba(59, 130, 246, 0.5); } + + .marker-icon-inner { + position: relative; z-index: 2; + width: 28px; height: 28px; + background: white; border-radius: 50%; + box-shadow: 0 2px 8px rgba(0,0,0,0.5); + display: flex; justify-content: center; align-items: center; + font-size: 14px; + } + + @keyframes pulse { + 0% { transform: scale(0.5); opacity: 1; } + 100% { transform: scale(1.5); opacity: 0; } + } @@ -275,9 +303,19 @@ if (filterText && !spbu.nama.toLowerCase().includes(filterText.toLowerCase()) && !spbu.no_spbu.toLowerCase().includes(filterText.toLowerCase())) return; const lat = parseFloat(spbu.latitude), lng = parseFloat(spbu.longitude); if (isNaN(lat) || isNaN(lng)) return; - const color = spbu.status === '24jam' ? '#22c55e' : '#f59e0b'; - const marker = L.circleMarker([lat, lng], { radius: 7, fillColor: color, color: '#fff', weight: 2, opacity: 1, fillOpacity: 0.9 }) - .bindPopup('' + spbu.nama + '
No: ' + spbu.no_spbu + '
Status: ' + (spbu.status === '24jam' ? '✅ 24 Jam' : '⏰ Tidak 24 Jam')) + + const is24 = spbu.status === '24jam'; + const haloClass = is24 ? 'spbu' : 'spbu-off'; + const customIcon = L.divIcon({ + className: 'custom-marker', + html: `
`, + iconSize: [28, 28], + iconAnchor: [14, 14], + popupAnchor: [0, -14] + }); + + const marker = L.marker([lat, lng], { icon: customIcon }) + .bindPopup('' + spbu.nama + '
No: ' + spbu.no_spbu + '
Status: ' + (is24 ? '✅ 24 Jam' : '⏰ Tidak 24 Jam')) .addTo(spbuLayer); bounds.push([lat, lng]); }); @@ -335,10 +373,11 @@ renderSPBU(spbu.nama.toLowerCase(), true); }; - const userIcon = L.icon({ - iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png', - shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png', - iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41] + const userIcon = L.divIcon({ + className: 'custom-marker', + html: `
📍
`, + iconSize: [28, 28], + iconAnchor: [14, 14] }); document.getElementById('btn-location').addEventListener('click', () => {