diff --git a/sigap-website/app/(pages)/(admin)/dashboard/crime-management/crime-overview/action.ts b/sigap-website/app/(pages)/(admin)/dashboard/crime-management/crime-overview/action.ts index 97db39e..27c540c 100644 --- a/sigap-website/app/(pages)/(admin)/dashboard/crime-management/crime-overview/action.ts +++ b/sigap-website/app/(pages)/(admin)/dashboard/crime-management/crime-overview/action.ts @@ -192,6 +192,27 @@ export async function getRecentIncidents(): Promise { time: "desc", }, include: { + user: { + select: { + id: true, + email: true, + phone: true, + role: { + select: { + id: true, + name: true, + } + }, + profile: { + select: { + username: true, + first_name: true, + last_name: true, + avatar: true, + } + } + }, + }, crime_categories: { select: { name: true, @@ -213,10 +234,22 @@ export async function getRecentIncidents(): Promise { }, }); + if (!incidents) { + console.error("No incidents found"); + return []; + } + + // Map DB result to IIncidentLogs interface return incidents.map((incident) => ({ id: incident.id, user_id: incident.user_id, + role_id: incident.user?.role?.id, + name: `${incident.user?.profile?.first_name} ${incident.user?.profile?.last_name}`, + email: incident.user?.email, + phone: incident.user?.phone ?? "", + role: incident.user?.role?.name, + avatar: incident.user?.profile?.avatar ?? "", latitude: incident.locations?.latitude ?? null, longitude: incident.locations?.longitude ?? null, district: incident.locations.districts.name ?? "", diff --git a/sigap-website/app/_components/map/layers/recent-incidents-layer.tsx b/sigap-website/app/_components/map/layers/recent-incidents-layer.tsx index 621f2c0..2228176 100644 --- a/sigap-website/app/_components/map/layers/recent-incidents-layer.tsx +++ b/sigap-website/app/_components/map/layers/recent-incidents-layer.tsx @@ -1,7 +1,9 @@ "use client" -import { useEffect, useCallback, useRef } from "react" +import { useEffect, useCallback, useRef, useState } from "react" import type { IIncidentLogs } from "@/app/_utils/types/crimes" +import { BASE_BEARING, BASE_DURATION, BASE_PITCH, ZOOM_3D } from "@/app/_utils/const/map" +import IncidentLogsPopup from "../pop-up/incident-logs-popup" interface RecentIncidentsLayerProps { visible?: boolean @@ -11,6 +13,8 @@ interface RecentIncidentsLayerProps { export default function RecentIncidentsLayer({ visible = false, map, incidents = [] }: RecentIncidentsLayerProps) { const isInteractingWithMarker = useRef(false) + const animationFrameRef = useRef(null) + const [selectedIncident, setSelectedIncident] = useState(null) // Filter incidents from the last 24 hours const recentIncidents = incidents.filter((incident) => { @@ -18,220 +22,342 @@ export default function RecentIncidentsLayer({ visible = false, map, incidents = const incidentDate = new Date(incident.timestamp) const now = new Date() const timeDiff = now.getTime() - incidentDate.getTime() - // 86400000 = 24 hours in milliseconds - return timeDiff <= 86400000 - }) + // 86400000 = 24 hours in milliseconds + return timeDiff <= 86400000 + }) + + // Split incidents into very recent (2 hours) and regular recent + const twoHoursInMs = 2 * 60 * 60 * 1000 // 2 hours in milliseconds const handleIncidentClick = useCallback( (e: any) => { - if (!map) return + if (!map) return - const features = map.queryRenderedFeatures(e.point, { layers: ["recent-incidents"] }) - if (!features || features.length === 0) return + const features = map.queryRenderedFeatures(e.point, { layers: ["recent-incidents"] }) + if (!features || features.length === 0) return - // Stop event propagation - e.originalEvent.stopPropagation() - e.preventDefault() + // Stop event propagation + e.originalEvent.stopPropagation() + e.preventDefault() - isInteractingWithMarker.current = true + isInteractingWithMarker.current = true - const incident = features[0] - if (!incident.properties) return + const incident = features[0] + if (!incident.properties) return - e.originalEvent.stopPropagation() - e.preventDefault() + e.originalEvent.stopPropagation() + e.preventDefault() - const incidentDetails = { - id: incident.properties.id, - description: incident.properties.description, - status: incident.properties?.status || "Active", - longitude: (incident.geometry as any).coordinates[0], - latitude: (incident.geometry as any).coordinates[1], - timestamp: new Date(incident.properties.timestamp || Date.now()), - category: incident.properties.category, - } + const incidentDetails = { + id: incident.properties.id, + description: incident.properties.description, + status: incident.properties?.status || "Active", + verified: incident.properties?.status, + longitude: (incident.geometry as any).coordinates[0], + latitude: (incident.geometry as any).coordinates[1], + timestamp: new Date(incident.properties.timestamp || Date.now()), + category: incident.properties.category, + address: incident.properties.address, + district: incident.properties.district, + severity: incident.properties.severity, + source: incident.properties.source, + user_id: incident.properties.user_id, + name: incident.properties.name, + email: incident.properties.email, + phone: incident.properties.telephone, + avatar: incident.properties.avatar, + role_id: incident.properties.role_id, + role: incident.properties.role, + isVeryRecent: incident.properties.isVeryRecent, + } - // console.log("Recent incident clicked:", incidentDetails) + // Fly to the incident location + map.flyTo({ + center: [incidentDetails.longitude, incidentDetails.latitude], + zoom: ZOOM_3D, + bearing: BASE_BEARING, + pitch: BASE_PITCH, + duration: BASE_DURATION, + }) - // Ensure markers stay visible - if (map.getLayer("recent-incidents")) { - map.setLayoutProperty("recent-incidents", "visibility", "visible") - } + // Set selected incident for the popup + setSelectedIncident(incidentDetails) - // First fly to the incident location - map.flyTo({ - center: [incidentDetails.longitude, incidentDetails.latitude], - zoom: 15, - bearing: 0, - pitch: 45, - duration: 2000, - }) + // Reset the flag after a delay + setTimeout(() => { + isInteractingWithMarker.current = false + }, 5000) + }, + [map], + ) - // Dispatch the incident_click event to show the popup - const customEvent = new CustomEvent("incident_click", { - detail: incidentDetails, - bubbles: true, - }) - - map.getCanvas().dispatchEvent(customEvent) - document.dispatchEvent(customEvent) - - // Reset the flag after a delay - setTimeout(() => { - isInteractingWithMarker.current = false - }, 5000) - }, - [map], - ) + // Handle popup close + const handleClosePopup = useCallback(() => { + setSelectedIncident(null) + }, []) useEffect(() => { - if (!map || !visible) return + if (!map || !visible) return - // console.log(`Setting up recent incidents layer with ${recentIncidents.length} incidents from the last 24 hours`) + // Convert incidents to GeoJSON with an additional property for recency + const now = new Date().getTime() - // Convert incidents to GeoJSON - const recentData = { - type: "FeatureCollection" as const, - features: recentIncidents.map((incident) => ({ - type: "Feature" as const, - geometry: { - type: "Point" as const, - coordinates: [incident.longitude, incident.latitude], - }, - properties: { - id: incident.id, - user_id: incident.user_id, - address: incident.address, - description: incident.description, - timestamp: incident.timestamp ? incident.timestamp.toString() : new Date().toString(), - category: incident.category, - district: incident.district, - severity: incident.severity, - status: incident.verified, - source: incident.source, - }, - })), - } + const recentData = { + type: "FeatureCollection" as const, + features: recentIncidents.map((incident) => { + const timestamp = incident.timestamp ? new Date(incident.timestamp).getTime() : now + const timeDiff = now - timestamp + const isVeryRecent = timeDiff <= twoHoursInMs - const setupLayerAndSource = () => { - try { - // Check if source exists and update it - if (map.getSource("recent-incidents-source")) { - ; (map.getSource("recent-incidents-source") as any).setData(recentData) - } else { - // If not, add source - map.addSource("recent-incidents-source", { - type: "geojson", - data: recentData, - }) + return { + type: "Feature" as const, + geometry: { + type: "Point" as const, + coordinates: [incident.longitude, incident.latitude], + }, + properties: { + id: incident.id, + role_id: incident.role_id, + user_id: incident.user_id, + name: incident.name, + email: incident.email, + telephone: incident.phone, + avatar: incident.avatar, + role: incident.role, + address: incident.address, + description: incident.description, + timestamp: incident.timestamp ? incident.timestamp.toString() : new Date().toString(), + category: incident.category, + district: incident.district, + severity: incident.severity, + status: incident.verified, + source: incident.source, + isVeryRecent: isVeryRecent, // Add this property to identify very recent incidents + timeDiff: timeDiff, // Time difference in milliseconds + }, + } + }), } - // Find first symbol layer for proper layering - const layers = map.getStyle().layers - let firstSymbolId: string | undefined - for (const layer of layers) { - if (layer.type === "symbol") { - firstSymbolId = layer.id - break + const setupLayerAndSource = () => { + try { + // Check if source exists and update it + if (map.getSource("recent-incidents-source")) { + ; (map.getSource("recent-incidents-source") as any).setData(recentData) + } else { + // If not, add source + map.addSource("recent-incidents-source", { + type: "geojson", + data: recentData, + }) + } + + // Find first symbol layer for proper layering + const layers = map.getStyle().layers + let firstSymbolId: string | undefined + for (const layer of layers) { + if (layer.type === "symbol") { + firstSymbolId = layer.id + break + } + } + + // Add the pulsing glow layer for very recent incidents (2 hours or less) + if (!map.getLayer("very-recent-incidents-pulse")) { + map.addLayer( + { + id: "very-recent-incidents-pulse", + type: "circle", + source: "recent-incidents-source", + filter: ["==", ["get", "isVeryRecent"], true], + paint: { + "circle-color": "#FF0000", + "circle-radius": [ + "interpolate", + ["linear"], + ["zoom"], + 7, + 8, + 12, + 16, + 15, + 24, + ], + "circle-opacity": 0.3, + "circle-stroke-width": 2, + "circle-stroke-color": "#FF0000", + "circle-stroke-opacity": 0.5, + }, + layout: { + visibility: visible ? "visible" : "none", + }, + }, + firstSymbolId, + ) + } else { + map.setLayoutProperty("very-recent-incidents-pulse", "visibility", visible ? "visible" : "none") + } + + // Add regular recent incidents glow + if (!map.getLayer("recent-incidents-glow")) { + map.addLayer( + { + id: "recent-incidents-glow", + type: "circle", + source: "recent-incidents-source", + paint: { + "circle-color": "#FF5252", + "circle-radius": ["interpolate", ["linear"], ["zoom"], 7, 6, 12, 12, 15, 18], + "circle-opacity": 0.2, + "circle-blur": 1, + }, + layout: { + visibility: visible ? "visible" : "none", + }, + }, + "very-recent-incidents-pulse", + ) + } else { + map.setLayoutProperty("recent-incidents-glow", "visibility", visible ? "visible" : "none") + } + + // Check if layer exists already for the main marker dots + if (!map.getLayer("recent-incidents")) { + map.addLayer( + { + id: "recent-incidents", + type: "circle", + source: "recent-incidents-source", + paint: { + "circle-color": [ + "case", + ["==", ["get", "isVeryRecent"], true], + "#FF0000", // Bright red for very recent + "#FF5252", // Standard red for older incidents + ], + "circle-radius": [ + "interpolate", + ["linear"], + ["zoom"], + 7, + 4, + 12, + 8, + 15, + 12, + ], + "circle-stroke-width": 2, + "circle-stroke-color": "#FFFFFF", + "circle-opacity": 0.8, + }, + layout: { + visibility: visible ? "visible" : "none", + }, + }, + "recent-incidents-glow", + ) + + // Add mouse events + map.on("mouseenter", "recent-incidents", () => { + map.getCanvas().style.cursor = "pointer" + }) + + map.on("mouseleave", "recent-incidents", () => { + map.getCanvas().style.cursor = "" + }) + } else { + // Update existing layer visibility + map.setLayoutProperty("recent-incidents", "visibility", visible ? "visible" : "none") + } + + // Create animation for very recent incidents + const animatePulse = () => { + if (!map || !map.getLayer("very-recent-incidents-pulse")) { + return + } + + // Create a pulsing effect by changing the size and opacity + const pulseSize = (Date.now() % 2000) / 2000 // Values from 0 to 1 every 2 seconds + const pulseOpacity = 0.7 - pulseSize * 0.5 // Opacity oscillates between 0.2 and 0.7 + const scaleFactor = 1 + pulseSize * 0.5 // Size oscillates between 1x and 1.5x + + map.setPaintProperty("very-recent-incidents-pulse", "circle-opacity", pulseOpacity) + map.setPaintProperty("very-recent-incidents-pulse", "circle-radius", [ + "interpolate", + ["linear"], + ["zoom"], + 7, + 8 * scaleFactor, + 12, + 16 * scaleFactor, + 15, + 24 * scaleFactor, + ]) + + // Continue animation + animationFrameRef.current = requestAnimationFrame(animatePulse) + } + + // Start animation if visible + if (visible) { + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current) + } + animationFrameRef.current = requestAnimationFrame(animatePulse) + } + + // Ensure click handler is properly registered + map.off("click", "recent-incidents", handleIncidentClick) + map.on("click", "recent-incidents", handleIncidentClick) + } catch (error) { + console.error("Error setting up recent incidents layer:", error) } } - // Check if layer exists already - if (!map.getLayer("recent-incidents")) { - map.addLayer( - { - id: "recent-incidents", - type: "circle", - source: "recent-incidents-source", - paint: { - "circle-color": "#FF5252", // Red color for recent incidents - "circle-radius": [ - "interpolate", - ["linear"], - ["zoom"], - 7, - 4, // Slightly larger at lower zooms for visibility - 12, - 8, - 15, - 12, // Larger maximum size - ], - "circle-stroke-width": 2, - "circle-stroke-color": "#FFFFFF", - "circle-opacity": 0.8, - // Add a pulsing effect - "circle-stroke-opacity": ["interpolate", ["linear"], ["zoom"], 7, 0.5, 15, 0.8], - }, - layout: { - visibility: visible ? "visible" : "none", - }, - }, - firstSymbolId, - ) - - // Add a glow effect with a larger circle behind - map.addLayer( - { - id: "recent-incidents-glow", - type: "circle", - source: "recent-incidents-source", - paint: { - "circle-color": "#FF5252", - "circle-radius": ["interpolate", ["linear"], ["zoom"], 7, 6, 12, 12, 15, 18], - "circle-opacity": 0.2, - "circle-blur": 1, - }, - layout: { - visibility: visible ? "visible" : "none", - }, - }, - "recent-incidents", - ) - - // Add mouse events - map.on("mouseenter", "recent-incidents", () => { - map.getCanvas().style.cursor = "pointer" - }) - - map.on("mouseleave", "recent-incidents", () => { - map.getCanvas().style.cursor = "" - }) - } else { - // Update existing layer visibility - map.setLayoutProperty("recent-incidents", "visibility", visible ? "visible" : "none") - map.setLayoutProperty("recent-incidents-glow", "visibility", visible ? "visible" : "none") - } - - // Ensure click handler is properly registered - map.off("click", "recent-incidents", handleIncidentClick) - map.on("click", "recent-incidents", handleIncidentClick) - } catch (error) { - console.error("Error setting up recent incidents layer:", error) - } - } - - // Check if style is loaded and set up layer accordingly - if (map.isStyleLoaded()) { - setupLayerAndSource() - } else { - map.once("style.load", setupLayerAndSource) - - // Fallback - setTimeout(() => { - if (map.isStyleLoaded()) { + // Check if style is loaded and set up layer accordingly + if (map.isStyleLoaded()) { setupLayerAndSource() } else { - console.warn("Map style still not loaded after timeout") + map.once("style.load", setupLayerAndSource) + + // Fallback + setTimeout(() => { + if (map.isStyleLoaded()) { + setupLayerAndSource() + } else { + console.warn("Map style still not loaded after timeout") + } + }, 1000) } - }, 1000) - } - return () => { - if (map) { - map.off("click", "recent-incidents", handleIncidentClick) - } - } - }, [map, visible, recentIncidents, handleIncidentClick]) + return () => { + if (map) { + map.off("click", "recent-incidents", handleIncidentClick) + } + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current) + } + } + }, [map, visible, recentIncidents, handleIncidentClick]) - return null + // Close popup when layer becomes invisible + useEffect(() => { + if (!visible) { + setSelectedIncident(null) + } + }, [visible]) + + return ( + <> + {/* Popup component */} + {selectedIncident && ( + + )} + + ) } diff --git a/sigap-website/app/_components/map/layers/timeline-layer.tsx b/sigap-website/app/_components/map/layers/timeline-layer.tsx index 5eb15d6..b4ab457 100644 --- a/sigap-website/app/_components/map/layers/timeline-layer.tsx +++ b/sigap-website/app/_components/map/layers/timeline-layer.tsx @@ -4,9 +4,10 @@ import { useEffect, useMemo, useState, useCallback } from "react" import { Layer, Source } from "react-map-gl/mapbox" import type { ICrimes } from "@/app/_utils/types/crimes" import type mapboxgl from "mapbox-gl" -import { format } from "date-fns" +import { format, getMonth, getYear, parseISO } from "date-fns" import { calculateAverageTimeOfDay } from "@/app/_utils/time" import TimelinePopup from "../pop-up/timeline-popup" +import { BASE_BEARING, BASE_DURATION, BASE_LATITUDE, BASE_LONGITUDE, BASE_PITCH, BASE_ZOOM, ZOOM_3D } from "@/app/_utils/const/map" interface TimelineLayerProps { crimes: ICrimes[] @@ -33,85 +34,154 @@ export default function TimelineLayer({ // Process district data to extract average incident times const districtTimeData = useMemo(() => { + // Convert year and month to numbers for comparison + const selectedYear = parseInt(year); + const selectedMonth = parseInt(month) - 1; // JS months are 0-indexed + const isMonthFiltered = month !== "all" && !isNaN(selectedMonth); + const isYearFiltered = !isNaN(selectedYear); + // Group incidents by district const districtGroups = new Map< string, { districtId: string districtName: string - incidents: Array<{ timestamp: Date; category: string }> + incidents: Array<{ + timestamp: Date; + category: string; + id?: string; + title?: string; + }> center: [number, number] + filteredIncidents: Array<{ + timestamp: Date; + category: string; + id?: string; + title?: string; + }> } >() - crimes.forEach((crime) => { - if (!crime.districts || !crime.district_id) return + crimes.forEach((crime) => { + if (!crime.districts || !crime.district_id) return - // Initialize district group if not exists - if (!districtGroups.has(crime.district_id)) { - // Find a central location for the district from any incident - const centerIncident = crime.crime_incidents.find((inc) => inc.locations?.latitude && inc.locations?.longitude) + // Initialize district group if not exists + if (!districtGroups.has(crime.district_id)) { + // Find a central location for the district + const centerIncident = crime.crime_incidents.find((inc) => inc.locations?.latitude && inc.locations?.longitude) - const center: [number, number] = centerIncident - ? [centerIncident.locations.longitude, centerIncident.locations.latitude] - : [0, 0] + const center: [number, number] = centerIncident + ? [centerIncident.locations.longitude, centerIncident.locations.latitude] + : [0, 0] - districtGroups.set(crime.district_id, { - districtId: crime.district_id, - districtName: crime.districts.name, - incidents: [], - center, - }) - } + districtGroups.set(crime.district_id, { + districtId: crime.district_id, + districtName: crime.districts.name, + incidents: [], + filteredIncidents: [], + center, + }) + } - // Filter incidents appropriately before adding - crime.crime_incidents.forEach((incident) => { - // Skip invalid incidents - if (!incident.timestamp) return - if (filterCategory !== "all" && incident.crime_categories.name !== filterCategory) return + // Add all incidents first (for all-time stats) + crime.crime_incidents.forEach((incident) => { + // Skip invalid incidents + if (!incident.timestamp) return - // Add to appropriate district group - const group = districtGroups.get(crime.district_id) - if (group) { - group.incidents.push({ - timestamp: new Date(incident.timestamp), - category: incident.crime_categories.name, - }) - } - }) - }) + const incidentDate = new Date(incident.timestamp); + const group = districtGroups.get(crime.district_id) - // Calculate average time for each district - const result = Array.from(districtGroups.values()) - .filter((group) => group.incidents.length > 0 && group.center[0] !== 0) - .map((group) => { - const avgTimeInfo = calculateAverageTimeOfDay(group.incidents.map((inc) => inc.timestamp)) + if (group) { + // Add to all incidents regardless of filters + group.incidents.push({ + timestamp: incidentDate, + category: incident.crime_categories.name, + id: incident.id, + title: incident.description || incident.crime_categories.name + }) - return { - id: group.districtId, - name: group.districtName, - center: group.center, - avgHour: avgTimeInfo.hour, - avgMinute: avgTimeInfo.minute, - formattedTime: avgTimeInfo.formattedTime, - timeDescription: avgTimeInfo.description, - totalIncidents: group.incidents.length, - timeOfDay: avgTimeInfo.timeOfDay, - earliestTime: format(avgTimeInfo.earliest, "p"), - latestTime: format(avgTimeInfo.latest, "p"), - mostFrequentHour: avgTimeInfo.mostFrequentHour, - categoryCounts: group.incidents.reduce( - (acc, inc) => { - acc[inc.category] = (acc[inc.category] || 0) + 1 - return acc - }, - {} as Record, - ), - } - }) + // Apply filters for filtered incidents + const incidentYear = getYear(incidentDate); + const incidentMonth = getMonth(incidentDate); - return result - }, [crimes, filterCategory, year, month]) + // Apply category filter + if (filterCategory !== "all" && incident.crime_categories.name !== filterCategory) return; + + // Apply year filter + if (isYearFiltered && incidentYear !== selectedYear) return; + + // Apply month filter + if (isMonthFiltered && incidentMonth !== selectedMonth) return; + + // Add to filtered incidents + group.filteredIncidents.push({ + timestamp: incidentDate, + category: incident.crime_categories.name, + id: incident.id, + title: incident.description || incident.crime_categories.name + }) + } + }) + }) + + // Calculate average time for each district + const result = Array.from(districtGroups.values()) + .filter((group) => { + // Only include districts that have incidents after filtering + const incidentsToUse = useAllData ? group.incidents : group.filteredIncidents; + return incidentsToUse.length > 0 && group.center[0] !== 0; + }) + .map((group) => { + // Choose which set of incidents to use + const incidentsToUse = useAllData ? group.incidents : group.filteredIncidents; + + // Calculate average times based on filtered or all incidents + const avgTimeInfo = calculateAverageTimeOfDay(incidentsToUse.map((inc) => inc.timestamp)) + + // Format incident data for display in timeline + const formattedIncidents = incidentsToUse + .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()) // Sort by most recent first + .map(incident => ({ + id: incident.id || Math.random().toString(36).substring(2), + title: incident.title || 'Incident', + time: format(incident.timestamp, 'MMM d, yyyy HH:mm'), + category: incident.category + })) + + return { + id: group.districtId, + name: group.districtName, + center: group.center, + avgHour: avgTimeInfo.hour, + avgMinute: avgTimeInfo.minute, + formattedTime: avgTimeInfo.formattedTime, + timeDescription: avgTimeInfo.description, + totalIncidents: incidentsToUse.length, + timeOfDay: avgTimeInfo.timeOfDay, + earliestTime: format(avgTimeInfo.earliest, "p"), + latestTime: format(avgTimeInfo.latest, "p"), + mostFrequentHour: avgTimeInfo.mostFrequentHour, + categoryCounts: incidentsToUse.reduce( + (acc, inc) => { + acc[inc.category] = (acc[inc.category] || 0) + 1 + return acc + }, + {} as Record, + ), + incidents: formattedIncidents, + selectedFilters: { + year: isYearFiltered ? selectedYear.toString() : "all", + month: isMonthFiltered ? (selectedMonth + 1).toString().padStart(2, '0') : "all", + category: filterCategory, + label: `${isYearFiltered ? selectedYear : "All years"}${isMonthFiltered ? ', ' + format(new Date(0, selectedMonth), 'MMMM') : ''}` + }, + allTimeCount: group.incidents.length, + useAllData: useAllData + } + }) + + return result + }, [crimes, filterCategory, year, month, useAllData]) // Convert processed data to GeoJSON for display const timelineGeoJSON = useMemo(() => { @@ -142,37 +212,45 @@ export default function TimelineLayer({ (e: mapboxgl.MapMouseEvent & { features?: mapboxgl.MapboxGeoJSONFeature[] }) => { if (!e.features || e.features.length === 0) return - // Stop event propagation - e.originalEvent.stopPropagation() - e.preventDefault() + // Stop event propagation + e.originalEvent.stopPropagation() + e.preventDefault() - const feature = e.features[0] - const props = feature.properties - if (!props) return + const feature = e.features[0] + const props = feature.properties + if (!props) return - // Get the corresponding district data for detailed info - const districtData = districtTimeData.find((d) => d.id === props.id) - if (!districtData) return + // Get the corresponding district data for detailed info + const districtData = districtTimeData.find((d) => d.id === props.id) + if (!districtData) return - // Fly to the location - if (map) { - map.flyTo({ - center: districtData.center, - zoom: 12, - duration: 1000, - pitch: 45, - bearing: 0, - }) - } + // Fly to the location + if (map) { + map.flyTo({ + center: districtData.center, + zoom: ZOOM_3D, + duration: BASE_DURATION, + pitch: BASE_PITCH, + bearing: BASE_BEARING, + }) + } - // Set the selected district for popup - setSelectedDistrict(districtData) - }, - [map, districtTimeData], - ) + // Set the selected district for popup + setSelectedDistrict(districtData) + }, + [map, districtTimeData], + ) // Handle popup close const handleClosePopup = useCallback(() => { + if (map) { + map.easeTo({ + zoom: BASE_ZOOM, + duration: BASE_DURATION, + pitch: BASE_PITCH, + bearing: BASE_BEARING, + }) + } setSelectedDistrict(null) }, []) @@ -180,45 +258,45 @@ export default function TimelineLayer({ useEffect(() => { if (!map || !visible) return - // Hide incident markers when timeline mode is activated - if (map.getLayer("unclustered-point")) { - map.setLayoutProperty("unclustered-point", "visibility", "none") - } + // Hide incident markers when timeline mode is activated + if (map.getLayer("unclustered-point")) { + map.setLayoutProperty("unclustered-point", "visibility", "none") + } - // Hide clusters when timeline mode is activated - if (map.getLayer("clusters")) { - map.setLayoutProperty("clusters", "visibility", "none") - } + // Hide clusters when timeline mode is activated + if (map.getLayer("clusters")) { + map.setLayoutProperty("clusters", "visibility", "none") + } - if (map.getLayer("cluster-count")) { - map.setLayoutProperty("cluster-count", "visibility", "none") - } + if (map.getLayer("cluster-count")) { + map.setLayoutProperty("cluster-count", "visibility", "none") + } - // Set up event handlers - const handleMouseEnter = () => { - if (map) map.getCanvas().style.cursor = "pointer" - } + // Set up event handlers + const handleMouseEnter = () => { + if (map) map.getCanvas().style.cursor = "pointer" + } - const handleMouseLeave = () => { - if (map) map.getCanvas().style.cursor = "" - } + const handleMouseLeave = () => { + if (map) map.getCanvas().style.cursor = "" + } - // Add event listeners - if (map.getLayer("timeline-markers")) { - map.on("click", "timeline-markers", handleMarkerClick) - map.on("mouseenter", "timeline-markers", handleMouseEnter) - map.on("mouseleave", "timeline-markers", handleMouseLeave) - } + // Add event listeners + if (map.getLayer("timeline-markers")) { + map.on("click", "timeline-markers", handleMarkerClick) + map.on("mouseenter", "timeline-markers", handleMouseEnter) + map.on("mouseleave", "timeline-markers", handleMouseLeave) + } - return () => { - // Clean up event listeners - if (map) { - map.off("click", "timeline-markers", handleMarkerClick) - map.off("mouseenter", "timeline-markers", handleMouseEnter) - map.off("mouseleave", "timeline-markers", handleMouseLeave) - } - } - }, [map, visible, handleMarkerClick]) + return () => { + // Clean up event listeners + if (map) { + map.off("click", "timeline-markers", handleMarkerClick) + map.off("mouseenter", "timeline-markers", handleMouseEnter) + map.off("mouseleave", "timeline-markers", handleMouseLeave) + } + } + }, [map, visible, handleMarkerClick]) // Clean up on unmount or when visibility changes useEffect(() => { @@ -250,49 +328,55 @@ export default function TimelineLayer({ "#263238", "#4CAF50", // Default color ], - "circle-radius": 18, + "circle-radius": [ + "interpolate", + ["linear"], + ["get", "totalIncidents"], + 1, 15, // Min size + 100, 25 // Max size + ], "circle-stroke-width": 2, "circle-stroke-color": "#000000", "circle-opacity": 0.9, }} /> - {/* Digital clock display */} - - + {/* Digital clock display */} + + - {/* Custom Popup Component */} - {selectedDistrict && ( - - )} - - ) + {/* Custom Popup Component */} + {selectedDistrict && ( + + )} + + ) } diff --git a/sigap-website/app/_components/map/pop-up/incident-logs-popup.tsx b/sigap-website/app/_components/map/pop-up/incident-logs-popup.tsx new file mode 100644 index 0000000..1e8d8b8 --- /dev/null +++ b/sigap-website/app/_components/map/pop-up/incident-logs-popup.tsx @@ -0,0 +1,255 @@ +"use client" + +import { Popup } from "react-map-gl/mapbox" +import { Badge } from "@/app/_components/ui/badge" +import { Card } from "@/app/_components/ui/card" +import { Separator } from "@/app/_components/ui/separator" +import { Button } from "@/app/_components/ui/button" +import { + Clock, + MapPin, + Navigation, + X, + AlertCircle, + Calendar, + User, + Tag, + Building2 +} from "lucide-react" +import { formatDistanceToNow } from "date-fns" +import { IconBrandGmail, IconPhone } from "@tabler/icons-react" + +interface IncidentLogsPopupProps { + longitude: number + latitude: number + onClose: () => void + incident: { + id: string + description?: string + category?: string + address?: string + timestamp: Date + district?: string + severity?: string + source?: string + status?: string + verified?: boolean | string + user_id?: string + name?: string + email?: string + phone?: string + avatar?: string + role_id?: string + role?: string + isVeryRecent?: boolean + } +} + +export default function IncidentLogsPopup({ + longitude, + latitude, + onClose, + incident, +}: IncidentLogsPopupProps) { + // Format timestamp in a human-readable way + const timeAgo = formatDistanceToNow(new Date(incident.timestamp), { addSuffix: true }) + + // Get severity badge color + const getSeverityColor = (severity?: string) => { + switch (severity?.toLowerCase()) { + case 'high': + return 'bg-red-500 text-white' + case 'medium': + return 'bg-orange-500 text-white' + case 'low': + return 'bg-yellow-500 text-black' + default: + return 'bg-gray-500 text-white' + } + } + + // Format verification status + const verificationStatus = typeof incident.verified === 'boolean' + ? incident.verified + : incident.verified === 'true' || incident.verified === '1' + + return ( + +
+ +
+ {/* Custom close button */} + + +
+

+ + {incident.category || "Incident Report"} +

+ + {incident.severity || "Unknown"} Priority + +
+ +
+ {incident.description && ( +
+

Description

+

{incident.description}

+
+ )} + +
+
+

Time

+

+ + {timeAgo} +

+
+ +
+

Status

+

+ + {verificationStatus ? "Verified" : "Unverified"} + +

+
+
+ + {incident.address && ( +
+

Location

+

+ + {incident.address} +

+
+ )} + + {incident.district && ( +
+

District

+

+ + {incident.district} +

+
+ )} + + {incident.source && ( +
+

Source

+

+ + {incident.source} +

+
+ )} + + {/* Reporter information section */} + {(incident.name || incident.user_id || incident.email || incident.phone) && ( +
+

Reporter Details

+
+ {incident.name && ( +

+ + {incident.name} + {incident.role && ( + + {incident.role} + + )} +

+ )} + + {incident.email && ( +

+ + {incident.email} +

+ )} + + {incident.phone && ( +

+ + {incident.phone} +

+ )} +
+
+ )} +
+ + + +
+

+ + Coordinates: {latitude.toFixed(6)}, {longitude.toFixed(6)} +

+

Incident ID: {incident.id}

+
+
+
+ {/* Connection line */} +
+ {/* Connection dot */} +
+ + {/* Pulsing effect for very recent incidents */} + {incident.isVeryRecent && ( +
+ )} +
+ + ) +} diff --git a/sigap-website/app/_components/map/pop-up/timeline-popup.tsx b/sigap-website/app/_components/map/pop-up/timeline-popup.tsx index d8e5b16..e2c6e58 100644 --- a/sigap-website/app/_components/map/pop-up/timeline-popup.tsx +++ b/sigap-website/app/_components/map/pop-up/timeline-popup.tsx @@ -1,7 +1,8 @@ "use client" +import { useState } from "react" import { Popup } from "react-map-gl/mapbox" -import { X } from 'lucide-react' +import { X, ChevronLeft, ChevronRight, Filter } from 'lucide-react' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../../ui/card" import { Button } from "../../ui/button" @@ -22,6 +23,20 @@ interface TimelinePopupProps { mostFrequentHour: number categoryCounts: Record timeOfDay: string + incidents?: Array<{ + id: string + title: string + time: string + category: string + }> + selectedFilters?: { + year: string + month: string + category: string + label: string + } + allTimeCount?: number + useAllData?: boolean } } @@ -31,6 +46,10 @@ export default function TimelinePopup({ onClose, district, }: TimelinePopupProps) { + // Pagination state + const [currentPage, setCurrentPage] = useState(1) + const itemsPerPage = 3 + // Get top 5 categories const topCategories = Object.entries(district.categoryCounts) .sort(([, countA], [, countB]) => countB - countA) @@ -52,6 +71,61 @@ export default function TimelinePopup({ } } + // Get text color for time of day + const getTextColorForTimeOfDay = (timeOfDay: string) => { + switch (timeOfDay) { + case "morning": + return "text-yellow-400" + case "afternoon": + return "text-orange-500" + case "evening": + return "text-indigo-600" + case "night": + return "text-slate-800" + default: + return "text-green-500" + } + } + + // Get paginated incidents + const getPaginatedIncidents = () => { + if (!district.incidents) return [] + + const startIndex = (currentPage - 1) * itemsPerPage + return district.incidents.slice(startIndex, startIndex + itemsPerPage) + } + + // Calculate total pages + const totalPages = district.incidents ? Math.ceil(district.incidents.length / itemsPerPage) : 0 + + // Handle page navigation + const goToNextPage = () => { + if (currentPage < totalPages) { + setCurrentPage(prev => prev + 1) + } + } + + const goToPrevPage = () => { + if (currentPage > 1) { + setCurrentPage(prev => prev - 1) + } + } + + // Get current incidents for display + const currentIncidents = getPaginatedIncidents() + + // Extract filter info + const filterLabel = district.selectedFilters?.label || "All data"; + const isFiltered = district.selectedFilters?.year !== "all" || district.selectedFilters?.month !== "all"; + const categoryFilter = district.selectedFilters?.category !== "all" + ? district.selectedFilters?.category + : null; + + // Get percentage of incidents in the time window compared to all time + const percentageOfAll = district.allTimeCount && district.allTimeCount > 0 && district.totalIncidents !== district.allTimeCount + ? Math.round((district.totalIncidents / district.allTimeCount) * 100) + : null; + return (
- - Average incident time analysis + + Average incident time analysis + {isFiltered && ( + + + {filterLabel} + + )} @@ -88,8 +168,16 @@ export default function TimelinePopup({ {district.timeDescription}
-
- Based on {district.totalIncidents} incidents +
+ Based on {district.totalIncidents} incidents + {percentageOfAll && ( + ({percentageOfAll}% of all time) + )} + {categoryFilter && ( + + {categoryFilter} + + )}
@@ -104,8 +192,8 @@ export default function TimelinePopup({
-
-
Top incident types:
+
+
Top incident types:
{topCategories.map(([category, count]) => (
@@ -115,6 +203,56 @@ export default function TimelinePopup({ ))}
+ + {district.incidents && district.incidents.length > 0 && ( +
+
+
Incidents Timeline:
+
+ {currentPage} of {totalPages} +
+
+ +
+ {currentIncidents.map((incident) => ( +
+
+ {incident.category} +
+
+ {incident.title} + + {incident.time} + +
+
+ ))} +
+ + {/* Pagination Controls */} +
+ + + +
+
+ )}
diff --git a/sigap-website/app/_components/map/pop-up/unit-popup.tsx b/sigap-website/app/_components/map/pop-up/unit-popup.tsx index 2cea43b..eb92a3c 100644 --- a/sigap-website/app/_components/map/pop-up/unit-popup.tsx +++ b/sigap-website/app/_components/map/pop-up/unit-popup.tsx @@ -61,7 +61,7 @@ export default function UnitPopup({ closeButton={false} closeOnClick={false} onClose={onClose} - anchor="top" + anchor="bottom" maxWidth="320px" className="unit-popup z-50" > diff --git a/sigap-website/app/_styles/ui.css b/sigap-website/app/_styles/ui.css index 000db04..a15539e 100644 --- a/sigap-website/app/_styles/ui.css +++ b/sigap-website/app/_styles/ui.css @@ -12,756 +12,783 @@ 1. ROOT COLOR VARIABLES ========================= */ :root { - --orange: #fa0; - --red: red; - --glow-rgb: 255, 102, 0; - --text-color: #fa0; - --danger-fill-color: #f23; - --danger-glow-rgb: 255, 0, 0; - --danger-text-color: #f23; - --gutter-size: 8px; + --orange: #fa0; + --red: red; + --glow-rgb: 255, 102, 0; + --text-color: #fa0; + --danger-fill-color: #f23; + --danger-glow-rgb: 255, 0, 0; + --danger-text-color: #f23; + --gutter-size: 8px; } /* ========================= 2. COLOR UTILITY CLASSES ========================= */ -.red-color { color: var(--red); } -.red-bg { background-color: var(--red); } -.red-border { border: 1px solid var(--red); } +.red-color { + color: var(--red); +} +.red-bg { + background-color: var(--red); +} +.red-border { + border: 1px solid var(--red); +} /* ========================= 3. STRIP BAR & ANIMATIONS ========================= */ /* --- Orange Stripe --- */ .strip-bar { - width: max(200vw,2000px); - height: 30px; - display: inline-block; - margin-bottom: -5px; + width: max(200vw, 2000px); + height: 30px; + display: inline-block; + margin-bottom: -5px; - --stripe-color: var(--orange); - --stripe-size: 15px; - --glow-color: rgba(255, 94, 0, .8); - --glow-size: 3px; - background-image: repeating-linear-gradient(-45deg, - var(--glow-color) calc(-1 * var(--glow-size)), - var(--stripe-color) 0, - var(--stripe-color) calc(var(--stripe-size) - var(--glow-size) / 2), - var(--glow-color) calc(var(--stripe-size) + var(--glow-size) / 2), - transparent calc(var(--stripe-size) + var(--glow-size) / 2), - transparent calc(2 * var(--stripe-size)), - var(--glow-color) calc(2 * var(--stripe-size) - var(--glow-size))); + --stripe-color: var(--orange); + --stripe-size: 15px; + --glow-color: rgba(255, 94, 0, 0.8); + --glow-size: 3px; + background-image: repeating-linear-gradient( + -45deg, + var(--glow-color) calc(-1 * var(--glow-size)), + var(--stripe-color) 0, + var(--stripe-color) calc(var(--stripe-size) - var(--glow-size) / 2), + var(--glow-color) calc(var(--stripe-size) + var(--glow-size) / 2), + transparent calc(var(--stripe-size) + var(--glow-size) / 2), + transparent calc(2 * var(--stripe-size)), + var(--glow-color) calc(2 * var(--stripe-size) - var(--glow-size)) + ); } /* --- Red Stripe --- */ .strip-bar-red { - width: max(200vw,2000px); - height: 30px; - display: inline-block; - margin-bottom: -5px; + width: max(200vw, 2000px); + height: 30px; + display: inline-block; + margin-bottom: -5px; - --stripe-color: var(--red); - --stripe-size: 15px; - --glow-color: rgba(255, 17, 0, 0.8); - --glow-size: 3px; - background-image: repeating-linear-gradient(-45deg, - var(--glow-color) calc(-1 * var(--glow-size)), - var(--stripe-color) 0, - var(--stripe-color) calc(var(--stripe-size) - var(--glow-size) / 2), - var(--glow-color) calc(var(--stripe-size) + var(--glow-size) / 2), - transparent calc(var(--stripe-size) + var(--glow-size) / 2), - transparent calc(2 * var(--stripe-size)), - var(--glow-color) calc(2 * var(--stripe-size) - var(--glow-size))); + --stripe-color: var(--red); + --stripe-size: 15px; + --glow-color: rgba(255, 17, 0, 0.8); + --glow-size: 3px; + background-image: repeating-linear-gradient( + -45deg, + var(--glow-color) calc(-1 * var(--glow-size)), + var(--stripe-color) 0, + var(--stripe-color) calc(var(--stripe-size) - var(--glow-size) / 2), + var(--glow-color) calc(var(--stripe-size) + var(--glow-size) / 2), + transparent calc(var(--stripe-size) + var(--glow-size) / 2), + transparent calc(2 * var(--stripe-size)), + var(--glow-color) calc(2 * var(--stripe-size) - var(--glow-size)) + ); } /* --- Vertical Orange Stripe --- */ .strip-bar-vertical { - height: 200vw; - transform: translate3d(0, 0, 0); - --stripe-color: var(--orange); - --stripe-size: 15px; - --glow-color: rgba(255, 94, 0, .8); - --glow-size: 3px; - background-image: repeating-linear-gradient(45deg, - var(--glow-color) calc(-1 * var(--glow-size)), - var(--stripe-color) 0, - var(--stripe-color) calc(var(--stripe-size) - var(--glow-size) / 2), - var(--glow-color) calc(var(--stripe-size) + var(--glow-size) / 2), - transparent calc(var(--stripe-size) + var(--glow-size) / 2), - transparent calc(2 * var(--stripe-size)), - var(--glow-color) calc(2 * var(--stripe-size) - var(--glow-size))); + height: 200vw; + transform: translate3d(0, 0, 0); + --stripe-color: var(--orange); + --stripe-size: 15px; + --glow-color: rgba(255, 94, 0, 0.8); + --glow-size: 3px; + background-image: repeating-linear-gradient( + 45deg, + var(--glow-color) calc(-1 * var(--glow-size)), + var(--stripe-color) 0, + var(--stripe-color) calc(var(--stripe-size) - var(--glow-size) / 2), + var(--glow-color) calc(var(--stripe-size) + var(--glow-size) / 2), + transparent calc(var(--stripe-size) + var(--glow-size) / 2), + transparent calc(2 * var(--stripe-size)), + var(--glow-color) calc(2 * var(--stripe-size) - var(--glow-size)) + ); } /* --- Vertical Red Stripe --- */ .strip-bar-red-vertical { - height: 200vw; - transform: translate3d(0, 0, 0); - --stripe-color: var(--red); - --stripe-size: 15px; - --glow-color: rgba(255, 17, 0, 0.8); - --glow-size: 3px; - background-image: repeating-linear-gradient(45deg, - var(--glow-color) calc(-1 * var(--glow-size)), - var(--stripe-color) 0, - var(--stripe-color) calc(var(--stripe-size) - var(--glow-size) / 2), - var(--glow-color) calc(var(--stripe-size) + var(--glow-size) / 2), - transparent calc(var(--stripe-size) + var(--glow-size) / 2), - transparent calc(2 * var(--stripe-size)), - var(--glow-color) calc(2 * var(--stripe-size) - var(--glow-size))); + height: 200vw; + transform: translate3d(0, 0, 0); + --stripe-color: var(--red); + --stripe-size: 15px; + --glow-color: rgba(255, 17, 0, 0.8); + --glow-size: 3px; + background-image: repeating-linear-gradient( + 45deg, + var(--glow-color) calc(-1 * var(--glow-size)), + var(--stripe-color) 0, + var(--stripe-color) calc(var(--stripe-size) - var(--glow-size) / 2), + var(--glow-color) calc(var(--stripe-size) + var(--glow-size) / 2), + transparent calc(var(--stripe-size) + var(--glow-size) / 2), + transparent calc(2 * var(--stripe-size)), + var(--glow-color) calc(2 * var(--stripe-size) - var(--glow-size)) + ); } /* --- Animations for Stripe --- */ @keyframes slideinBg { - from {background-position: top; } - to {background-position: -100px 0px;} + from { + background-position: top; + } + to { + background-position: -100px 0px; + } } .strip-animation-vertical { - animation: stripAnimationVertical 15s infinite linear; + animation: stripAnimationVertical 15s infinite linear; } .strip-animation-vertical-reverse { - animation: stripAnimationVertical 15s infinite linear reverse; + animation: stripAnimationVertical 15s infinite linear reverse; } .strip-animation { - animation: stripAnimation 10s infinite linear; + animation: stripAnimation 10s infinite linear; } .strip-animation-reverse { - animation: stripAnimation 10s infinite linear reverse; + animation: stripAnimation 10s infinite linear reverse; } @keyframes stripAnimationVertical { - 100% { - transform: translateY(-66%); - } + 100% { + transform: translateY(-66%); + } } @keyframes stripAnimation { - 100% { - transform: translateX(-66%); - } + 100% { + transform: translateX(-66%); + } } /* ========================= 4. STRIP & MARQUEE EFFECT ========================= */ .strip { - background-color: black; - width: 100vw; - border-top: 1px solid var(--red); - border-bottom: 1px solid var(--red); - position: fixed; + background-color: black; + width: 100vw; + border-top: 1px solid var(--red); + border-bottom: 1px solid var(--red); + position: fixed; } .strip-wrapper { - width: max(200vw,2000px); - overflow: hidden; - white-space: nowrap; + width: max(200vw, 2000px); + overflow: hidden; + white-space: nowrap; } @keyframes marquee1 { - 0% { - transform: translateX(100%); - } + 0% { + transform: translateX(100%); + } - 100% { - transform: translateX(-100%); - } + 100% { + transform: translateX(-100%); + } } @keyframes marquee2 { - from { - transform: translateX(0%); - } + from { + transform: translateX(0%); + } - to { - transform: translateX(-200%); - } + to { + transform: translateX(-200%); + } } .loop-strip { - animation: loopStrip infinite linear; - animation-duration: 10s; + animation: loopStrip infinite linear; + animation-duration: 10s; } .loop-strip-reverse { - animation: loopStrip infinite linear reverse; - animation-duration: 10s; + animation: loopStrip infinite linear reverse; + animation-duration: 10s; } .anim-duration-10 { - animation-duration: 10s !important; + animation-duration: 10s !important; } .anim-duration-20 { - animation-duration: 20s !important; + animation-duration: 20s !important; } @keyframes loopStrip { - from { - transform: translateX(0); - } + from { + transform: translateX(0); + } - to { - transform: translateX(-100%); - } + to { + transform: translateX(-100%); + } } /* ========================= 5. POPUP & TRANSITION ANIMATION ========================= */ .show-pop-up { - animation: showPopUp 0.3s ease-in-out forwards; + animation: showPopUp 0.3s ease-in-out forwards; } @keyframes showPopUp { - 0% { - opacity: 0; - transform: scale(0.5); - } + 0% { + opacity: 0; + transform: scale(0.5); + } - 100% { - opacity: 1; - transform: scale(1); - } + 100% { + opacity: 1; + transform: scale(1); + } } .close-pop-up { - animation: closePopUp 0.3s ease-in-out forwards !important; + animation: closePopUp 0.3s ease-in-out forwards !important; } @keyframes closePopUp { - 0% { - opacity: 1; - transform: scale(1); - } + 0% { + opacity: 1; + transform: scale(1); + } - 100% { - opacity: 0; - transform: scale(0.5); - } + 100% { + opacity: 0; + transform: scale(0.5); + } } .vertical-reveal { - animation: verticalReveal 0.3s ease-in-out; + animation: verticalReveal 0.3s ease-in-out; } @keyframes verticalReveal { - 0% { - transform: scaleY(0); - } + 0% { + transform: scaleY(0); + } - 100% { - transform: scaleY(1); - } + 100% { + transform: scaleY(1); + } } /* ========================= 6. GLOW & BLINK EFFECTS ========================= */ .glow-effect { - animation: glowEffect 1s infinite; + animation: glowEffect 1s infinite; } @keyframes glowEffect { - 0% { - -webkit-box-shadow: 0px 0px 66px 17px rgba(252, 60, 22, 0.59); - -moz-box-shadow: 0px 0px 66px 17px rgba(252, 60, 22, 0.59); - box-shadow: 0px 0px 66px 17px rgba(252, 60, 22, 0.59); - } + 0% { + -webkit-box-shadow: 0px 0px 66px 17px rgba(252, 60, 22, 0.59); + -moz-box-shadow: 0px 0px 66px 17px rgba(252, 60, 22, 0.59); + box-shadow: 0px 0px 66px 17px rgba(252, 60, 22, 0.59); + } - 50% { - -webkit-box-shadow: 0px 0px 66px 44px rgba(252, 60, 22, 0.9); - -moz-box-shadow: 0px 0px 66px 44px rgba(252, 60, 22, 0.9); - box-shadow: 0px 0px 66px 44px rgba(252, 60, 22, 0.9); - } + 50% { + -webkit-box-shadow: 0px 0px 66px 44px rgba(252, 60, 22, 0.9); + -moz-box-shadow: 0px 0px 66px 44px rgba(252, 60, 22, 0.9); + box-shadow: 0px 0px 66px 44px rgba(252, 60, 22, 0.9); + } - 100% { - -webkit-box-shadow: 0px 0px 66px 17px rgba(252, 60, 22, 0.59); - -moz-box-shadow: 0px 0px 66px 17px rgba(252, 60, 22, 0.59); - box-shadow: 0px 0px 66px 17px rgba(252, 60, 22, 0.59); - } + 100% { + -webkit-box-shadow: 0px 0px 66px 17px rgba(252, 60, 22, 0.59); + -moz-box-shadow: 0px 0px 66px 17px rgba(252, 60, 22, 0.59); + box-shadow: 0px 0px 66px 17px rgba(252, 60, 22, 0.59); + } } .blink { - animation: blink 1s infinite; + animation: blink 1s infinite; } @keyframes blink { - 0% { - opacity: 0; - } + 0% { + opacity: 0; + } - 50% { - opacity: 1; - } + 50% { + opacity: 1; + } - 100% { - opacity: 0; - } + 100% { + opacity: 0; + } } /* ========================= 7. MARKER & MAPBOX STYLES ========================= */ .marker-daerah { - width: auto; - height: 25px; + width: auto; + height: 25px; - cursor: pointer; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; + cursor: pointer; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; } .marker-daerah p { - border: 1px black solid; - color: black; - background-color: red; - padding: 2px; - font-size: 8px; - text-transform: uppercase; - max-width: 75px; - line-height: 1; - text-align: center; - font-weight: bold; + border: 1px black solid; + color: black; + background-color: red; + padding: 2px; + font-size: 8px; + text-transform: uppercase; + max-width: 75px; + line-height: 1; + text-align: center; + font-weight: bold; } .marker-gempa { - font-size: 20px; - color: red; - cursor: pointer; + font-size: 20px; + color: red; + cursor: pointer; } .marker-gempa-wave { - border: 3px red solid; - border-radius: 50%; - width: 50px; - height: 50px; - font-size: 20px; - color: red; - cursor: pointer; + border: 3px red solid; + border-radius: 50%; + width: 50px; + height: 50px; + font-size: 20px; + color: red; + cursor: pointer; } .mapboxgl-popup-close-button { - display: none !important; + display: none !important; } .mapboxgl-popup { - width: auto; - + width: auto; } .mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip { - border-top-color: unset; - height: 70px; - width: 3px; - background-color: white; - border: unset; + border-top-color: unset; + height: 70px; + width: 3px; + background-color: white; + border: unset; } .mapboxgl-popup-anchor-top .mapboxgl-popup-tip { - border-top-color: unset; - height: 70px; - width: 3px; - background-color: white; - border: unset; + border-top-color: unset; + height: 70px; + width: 3px; + background-color: white; + border: unset; } .mapboxgl-popup-content { - background-color: unset; - border: unset; - border-radius: 0.5rem !important; - padding: 0 !important; - max-width: 320px; - box-shadow: - 0 10px 15px -3px rgba(0, 0, 0, 0.1), - 0 4px 6px -2px rgba(0, 0, 0, 0.05) !important; - overflow: hidden; + background-color: unset; + border: unset; + border-radius: 0.5rem !important; + padding: 0 !important; + max-width: 320px; + box-shadow: + 0 10px 15px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -2px rgba(0, 0, 0, 0.05) !important; + overflow: hidden; } /* ========================= 8. WARNING & SHAPE COMPONENTS ========================= */ .warning-wrapper { - display: flex; - justify-content: center; - position: absolute; - height: 200px; - max-width: 50%; - margin: auto; - top: 0; - bottom: 0; - left: 0; - right: 0; - animation: showWarningAlert 0.3s ease-in-out forwards; - flex-direction: column; - align-items: center; + display: flex; + justify-content: center; + position: absolute; + height: 200px; + max-width: 50%; + margin: auto; + top: 0; + bottom: 0; + left: 0; + right: 0; + animation: showWarningAlert 0.3s ease-in-out forwards; + flex-direction: column; + align-items: center; } .long-shape { - position: relative; - width: 500px; - display: flex; - justify-content: center; + position: relative; + width: 500px; + display: flex; + justify-content: center; } .long-shape .shape { - height: 150px; - width: 300px; - display: flex; - justify-content: space-between; + height: 150px; + width: 300px; + display: flex; + justify-content: space-between; } .shape { - position: absolute; - margin: auto; + position: absolute; + margin: auto; } .long-shape .bg { - background-color: #e60003; + background-color: #e60003; } .long-shape .fg { - background-color: #e60003; - scale: 0.98 0.92; + background-color: #e60003; + scale: 0.98 0.92; } .long-shape .br { - background-color: black; - scale: 0.99 0.96; + background-color: black; + scale: 0.99 0.96; } .long-shape .hex { - margin-top: 30px; - transform: scale(1.5); + margin-top: 30px; + transform: scale(1.5); } .basic-shape { - height: 100px; - width: 115px; - transform: scale(1.5); - z-index: 99; + height: 100px; + width: 115px; + transform: scale(1.5); + z-index: 99; } .basic-shape .hex { - position: absolute; - margin: auto; + position: absolute; + margin: auto; } .basic-shape .hex:nth-child(1) { - scale: 0.95; + scale: 0.95; } .basic-shape .hex:nth-child(2) { - scale: 0.9; + scale: 0.9; } .basic-shape .hex:nth-child(3) { - scale: 0.85; + scale: 0.85; } .basic-shape .hex:nth-child(4) { - scale: 0.8; + scale: 0.8; } .shape .hex:nth-child(1) { - margin-left: -20%; + margin-left: -20%; } .shape .hex:nth-child(2) { - margin-right: -20%; + margin-right: -20%; } .warning { - height: 500px; - width: 450px; + height: 500px; + width: 450px; } .long-hex { - position: relative; - height: 150px; - width: 275px; - background-image: url('/images/long_shape.svg'); - background-size: contain; - background-position: center; - background-repeat: no-repeat; + position: relative; + height: 150px; + width: 275px; + background-image: url('/images/long_shape.svg'); + background-size: contain; + background-position: center; + background-repeat: no-repeat; } .warning-black-hex { - position: relative; - height: 100px; - width: 100px; - background-image: url('/images/warning_shape_black.svg'); - background-size: contain; - background-position: center; - background-repeat: no-repeat; + position: relative; + height: 100px; + width: 100px; + background-image: url('/images/warning_shape_black.svg'); + background-size: contain; + background-position: center; + background-repeat: no-repeat; } .warning-black { - position: relative; - height: 40px; - width: 40px; - background-image: url('/images/warning_gempa_black.png'); - background-size: contain; - background-position: center; - background-repeat: no-repeat; + position: relative; + height: 40px; + width: 40px; + background-image: url('/images/warning_gempa_black.png'); + background-size: contain; + background-position: center; + background-repeat: no-repeat; } .warning-yellow { - position: relative; - height: 80px; - width: 50px; - background-image: url('/images/warning_gempa_red_yellow.svg'); - background-size: contain; - background-position: center; - background-repeat: no-repeat; + position: relative; + height: 80px; + width: 50px; + background-image: url('/images/warning_gempa_red_yellow.svg'); + background-size: contain; + background-position: center; + background-repeat: no-repeat; } .warning-tsunami-yellow { - position: relative; - height: 80px; - width: 50px; - background-image: url('/images/warning_tsunami_yellow.png'); - background-size: contain; - background-position: center; - background-repeat: no-repeat; + position: relative; + height: 80px; + width: 50px; + background-image: url('/images/warning_tsunami_yellow.png'); + background-size: contain; + background-position: center; + background-repeat: no-repeat; } .basic-hex { - position: relative; - height: 100px; - width: 100px; - background-image: url('/images/hex_shape.svg'); - background-size: contain; - background-position: center; - background-repeat: no-repeat; + position: relative; + height: 100px; + width: 100px; + background-image: url('/images/hex_shape.svg'); + background-size: contain; + background-position: center; + background-repeat: no-repeat; } .animation-delay-1 { - animation-delay: 1s; + animation-delay: 1s; } .animation-delay-2 { - animation-delay: 2s; + animation-delay: 2s; } .animation-delay-3 { - animation-delay: 3s; + animation-delay: 3s; } .animation-delay-4 { - animation-delay: 4s; + animation-delay: 4s; } .warning .info .basic-hex:nth-child(1) { - animation-delay: 2s; + animation-delay: 2s; } .warning .info .basic-hex:nth-child(2) { - animation-delay: 2.2s; + animation-delay: 2.2s; } .warning .info .basic-hex:nth-child(3) { - animation-delay: 2.4s; + animation-delay: 2.4s; } .animation-fast { - animation-duration: 0.5s; + animation-duration: 0.5s; } .blink-fast { - animation-duration: 0.1s; + animation-duration: 0.1s; } /* ========================= 9. OVERLAY & LIST COMPONENTS ========================= */ .overlay-bg { - background-color: rgba(0, 0, 0, 0.8); + background-color: rgba(0, 0, 0, 0.8); } .list-event { - display: block; - font: 400 16px 'Roboto Condensed'; - letter-spacing: -1px; - line-height: 1; - padding: 1px calc(var(--gutter-size) - 3px); - text-transform: uppercase; - user-select: none; - white-space: nowrap; - --text-glow-color: rgba(var(--glow-rgb), .5); - color: var(--text-color); + display: block; + font: 400 16px 'Roboto Condensed'; + letter-spacing: -1px; + line-height: 1; + padding: 1px calc(var(--gutter-size) - 3px); + text-transform: uppercase; + user-select: none; + white-space: nowrap; + --text-glow-color: rgba(var(--glow-rgb), 0.5); + color: var(--text-color); } .text-glow-red { - --text-glow-color: rgba(var(--danger-glow-rgb), 0.5); - color: var(--danger-text-color); + --text-glow-color: rgba(var(--danger-glow-rgb), 0.5); + color: var(--danger-text-color); } .text-glow { - --text-glow-color: rgba(var(--glow-rgb), 0.5); - color: var(--text-color) !important; + --text-glow-color: rgba(var(--glow-rgb), 0.5); + color: var(--text-color) !important; } .bordered { - color: var(--text-color); - --border-glow-color: rgba(var(--glow-rgb), 0.7); - border-radius: var(--gutter-size); - border-style: solid; - border-width: 1px; - border-color: unset; - box-shadow: inset 0 0 0 1px var(--border-glow-color), 0 0 0 1px var(--border-glow-color); + color: var(--text-color); + --border-glow-color: rgba(var(--glow-rgb), 0.7); + border-radius: var(--gutter-size); + border-style: solid; + border-width: 1px; + border-color: unset; + box-shadow: + inset 0 0 0 1px var(--border-glow-color), + 0 0 0 1px var(--border-glow-color); } .red-bordered { - color: var(--danger-text-color); - --border-glow-color: rgba(var(--danger-glow-rgb), 0.7); - border-radius: var(--gutter-size); - border-style: solid; - border-width: 1px; - border-color: unset; - box-shadow: inset 0 0 0 1px var(--border-glow-color), 0 0 0 1px var(--border-glow-color); + color: var(--danger-text-color); + --border-glow-color: rgba(var(--danger-glow-rgb), 0.7); + border-radius: var(--gutter-size); + border-style: solid; + border-width: 1px; + border-color: unset; + box-shadow: + inset 0 0 0 1px var(--border-glow-color), + 0 0 0 1px var(--border-glow-color); } .red-bordered-bottom { - color: var(--danger-text-color); - --border-glow-color: rgba(var(--danger-glow-rgb), 0.7); - border-color: unset; - border-bottom: 1px solid red; - box-shadow: inset 0 0 0 1px var(--border-glow-color), 0 0 0 1px var(--border-glow-color); + color: var(--danger-text-color); + --border-glow-color: rgba(var(--danger-glow-rgb), 0.7); + border-color: unset; + border-bottom: 1px solid red; + box-shadow: + inset 0 0 0 1px var(--border-glow-color), + 0 0 0 1px var(--border-glow-color); } .red-bordered-top { - color: var(--danger-text-color); - --border-glow-color: rgba(var(--danger-glow-rgb), 0.7); - border-color: unset; - border-top: 1px solid var(--danger-glow-rgb); - box-shadow: inset 0 0 0 1px var(--border-glow-color), 0 0 0 1px var(--border-glow-color); + color: var(--danger-text-color); + --border-glow-color: rgba(var(--danger-glow-rgb), 0.7); + border-color: unset; + border-top: 1px solid var(--danger-glow-rgb); + box-shadow: + inset 0 0 0 1px var(--border-glow-color), + 0 0 0 1px var(--border-glow-color); } /* ========================= 10. CARD COMPONENTS ========================= */ .card { - background-color: black; - transition: 0.3s; + background-color: black; + transition: 0.3s; } .card-header { - padding: 6px; - color: var(--orange); - position: relative; - border-radius: 10px 10px 0px 0px; + padding: 6px; + color: var(--orange); + position: relative; + border-radius: 10px 10px 0px 0px; } .card-footer { - padding: 6px; - border-top: 3px var(--red) solid; - color: var(--orange); - position: relative; - border-radius: 0px 0px 10px 10px; + padding: 6px; + border-top: 3px var(--red) solid; + color: var(--orange); + position: relative; + border-radius: 0px 0px 10px 10px; } .card-content { - padding: 12px; + padding: 12px; } .card-float { - transition: all 0.3s ease-in-out; + transition: all 0.3s ease-in-out; } .card-float .card-content { - display: block; - max-height: 45vh; - overflow-y: auto; - overflow-x: hidden; + display: block; + max-height: 45vh; + overflow-y: auto; + overflow-x: hidden; } /* ========================= 11. JAJAR GENJANG & DAERAH LIST ========================= */ .jajar-genjang { - height: 30px; - width: 100%; - transform: skew(15deg); - -webkit-transform: skew(15deg); - -moz-transform: skew(15deg); - -o-transform: skew(15deg); - background-color: var(--orange); - -webkit-box-shadow: 0px 0px 5px 0px rgba(252, 114, 22, 1); - -moz-box-shadow: 0px 0px 5px 0px rgba(252, 114, 22, 1); - box-shadow: 0px 0px 5px 0px rgba(252, 114, 22, 1); - display: flex; - align-items: center; - padding: 6px; - overflow: hidden; + height: 30px; + width: 100%; + transform: skew(15deg); + -webkit-transform: skew(15deg); + -moz-transform: skew(15deg); + -o-transform: skew(15deg); + background-color: var(--orange); + -webkit-box-shadow: 0px 0px 5px 0px rgba(252, 114, 22, 1); + -moz-box-shadow: 0px 0px 5px 0px rgba(252, 114, 22, 1); + box-shadow: 0px 0px 5px 0px rgba(252, 114, 22, 1); + display: flex; + align-items: center; + padding: 6px; + overflow: hidden; } -.jajar-genjang .time-countdown {} +.jajar-genjang .time-countdown { +} .jajar-genjang.danger { - background-color: var(--red); - -webkit-box-shadow: 0px 0px 5px 0px rgba(250, 23, 23, 1); - -moz-box-shadow: 0px 0px 5px 0px rgba(250, 23, 23, 1); - box-shadow: 0px 0px 5px 0px rgba(250, 23, 23, 1); + background-color: var(--red); + -webkit-box-shadow: 0px 0px 5px 0px rgba(250, 23, 23, 1); + -moz-box-shadow: 0px 0px 5px 0px rgba(250, 23, 23, 1); + box-shadow: 0px 0px 5px 0px rgba(250, 23, 23, 1); } .jajar-genjang p { - transform: skew(-15deg); - -webkit-transform: skew(-15deg); - -moz-transform: skew(-15deg); - -o-transform: skew(-15deg); - color: black; - font-weight: bold; - font-size: 14px; + transform: skew(-15deg); + -webkit-transform: skew(-15deg); + -moz-transform: skew(-15deg); + -o-transform: skew(-15deg); + color: black; + font-weight: bold; + font-size: 14px; } .pinggir-jajar-genjang { - height: 30px; - width: 30px; - transform: skew(15deg); - -webkit-transform: skew(15deg); - -moz-transform: skew(15deg); - -o-transform: skew(15deg); + height: 30px; + width: 30px; + transform: skew(15deg); + -webkit-transform: skew(15deg); + -moz-transform: skew(15deg); + -o-transform: skew(15deg); } .item-daerah { - width: 100%; - position: relative; + width: 100%; + position: relative; } .list-daerah .card-content { - max-height: 50vh; - overflow-y: auto; + max-height: 50vh; + overflow-y: auto; } -.item-daerah.danger {} +.item-daerah.danger { +} .item-daerah .content { - position: absolute; - font-size: 12px; - color: black; - font-weight: bold; + position: absolute; + font-size: 12px; + color: black; + font-weight: bold; } .item-daerah .pinggir-jajar-genjang { - background-color: var(--orange); + background-color: var(--orange); } .item-daerah.danger .pinggir-jajar-genjang { - background-color: var(--red); + background-color: var(--red); } .time-countdown { - font-family: 'DS-Digital'; + font-family: 'DS-Digital'; } .text-time { - font-family: 'DS-Digital'; + font-family: 'DS-Digital'; } /* ========================= @@ -788,200 +815,206 @@ 13. SLIDE ANIMATION ========================= */ .slide-in-left { - animation: slideInLeft 0.5s forwards; + animation: slideInLeft 0.5s forwards; } @keyframes slideInLeft { - 0% { - transform: translateX(-100%); - } + 0% { + transform: translateX(-100%); + } - 100% { - transform: translateX(0); - } + 100% { + transform: translateX(0); + } } /* ========================= 14. LABEL & RESPONSIVE ========================= */ label#internal { - --decal-width: 50px; - --label-corner-size: 3px; - --label-gutter-size: 5px; + --decal-width: 50px; + --label-corner-size: 3px; + --label-gutter-size: 5px; } .label { - overflow: hidden; - font: 400 2rem 'Roboto Condensed'; - letter-spacing: -1px; - line-height: 1; - padding-right: 0px; - text-transform: uppercase; - user-select: none; - white-space: nowrap; - --text-glow-color: rgba(var(--glow-rgb), .5); - color: var(--text-color); - text-shadow: -1px 1px 0 var(--text-glow-color), 1px -1px 0 var(--text-glow-color), -1px -1px 0 var(--text-glow-color), 1px 1px 0 var(--text-glow-color); + overflow: hidden; + font: 400 2rem 'Roboto Condensed'; + letter-spacing: -1px; + line-height: 1; + padding-right: 0px; + text-transform: uppercase; + user-select: none; + white-space: nowrap; + --text-glow-color: rgba(var(--glow-rgb), 0.5); + color: var(--text-color); + text-shadow: + -1px 1px 0 var(--text-glow-color), + 1px -1px 0 var(--text-glow-color), + -1px -1px 0 var(--text-glow-color), + 1px 1px 0 var(--text-glow-color); } .label#internal .decal { - border-radius: calc(var(--label-corner-size) - 1px); - display: block; - height: 100px; - width: 100%; + border-radius: calc(var(--label-corner-size) - 1px); + display: block; + height: 100px; + width: 100%; } .-striped { - --stripe-color: var(--danger-fill-color); - --stripe-size: 15px; - --glow-color: rgba(var(--danger-glow-rgb), .8); - --glow-size: 3px; - background-image: repeating-linear-gradient(-45deg, - var(--glow-color) calc(-1 * var(--glow-size)), - var(--stripe-color) 0, - var(--stripe-color) calc(var(--stripe-size) - var(--glow-size) / 2), - var(--glow-color) calc(var(--stripe-size) + var(--glow-size) / 2), - transparent calc(var(--stripe-size) + var(--glow-size) / 2), - transparent calc(2 * var(--stripe-size)), - var(--glow-color) calc(2 * var(--stripe-size) - var(--glow-size))); - box-shadow: inset 0 0 1px calc(var(--glow-size) / 2) var(--shade-3); + --stripe-color: var(--danger-fill-color); + --stripe-size: 15px; + --glow-color: rgba(var(--danger-glow-rgb), 0.8); + --glow-size: 3px; + background-image: repeating-linear-gradient( + -45deg, + var(--glow-color) calc(-1 * var(--glow-size)), + var(--stripe-color) 0, + var(--stripe-color) calc(var(--stripe-size) - var(--glow-size) / 2), + var(--glow-color) calc(var(--stripe-size) + var(--glow-size) / 2), + transparent calc(var(--stripe-size) + var(--glow-size) / 2), + transparent calc(2 * var(--stripe-size)), + var(--glow-color) calc(2 * var(--stripe-size) - var(--glow-size)) + ); + box-shadow: inset 0 0 1px calc(var(--glow-size) / 2) var(--shade-3); } .-blink { - animation-name: blink; - animation-duration: var(--blink-duration); - animation-iteration-count: infinite; - animation-timing-function: steps(1); + animation-name: blink; + animation-duration: var(--blink-duration); + animation-iteration-count: infinite; + animation-timing-function: steps(1); } .label#internal .text.-characters { - font-size: 3.5rem; - padding-top: var(--label-gutter-size); + font-size: 3.5rem; + padding-top: var(--label-gutter-size); } /* --- Responsive for Mobile --- */ @media (max-width: 768px) { - .card-float .card-content { - height: 0px; - padding: 0px; - } + .card-float .card-content { + height: 0px; + padding: 0px; + } - .card-float.open .card-content { - height: unset; - padding: 6px; - } + .card-float.open .card-content { + height: unset; + padding: 6px; + } - .card-float { - margin: auto; - right: 0.25rem; - left: 0.25rem; - } + .card-float { + margin: auto; + right: 0.25rem; + left: 0.25rem; + } - .label#internal .decal { - width: 40px; - } + .label#internal .decal { + width: 40px; + } - .card-header { - cursor: pointer; - } + .card-header { + cursor: pointer; + } } /* --- Responsive for Tablet --- */ @media (min-width: 768px) and (max-width: 1024px) { - .label#internal .text.-characters { - font-size: 3.5rem; - } + .label#internal .text.-characters { + font-size: 3.5rem; + } - .label#internal .text { - font-size: 2.5rem; - } + .label#internal .text { + font-size: 2.5rem; + } - .label#internal .decal { - width: 40px; - } + .label#internal .decal { + width: 40px; + } } /* ========================= 15. ICONS & LOADER ========================= */ .github-icon { - width: 20px; - height: 20px; - border-radius: 50%; - background-image: url('https://cdn.jsdelivr.net/gh/devicons/devicon/icons/github/github-original.svg'); - background-color: white; - background-repeat: no-repeat; - background-position: center; + width: 20px; + height: 20px; + border-radius: 50%; + background-image: url('https://cdn.jsdelivr.net/gh/devicons/devicon/icons/github/github-original.svg'); + background-color: white; + background-repeat: no-repeat; + background-position: center; } .bmkg-icon { - width: 25px; - height: 25px; - border-radius: 50%; - background-image: url('/images/logo-bmkg.webp'); - background-size: contain; - background-repeat: no-repeat; - background-position: center; + width: 25px; + height: 25px; + border-radius: 50%; + background-image: url('/images/logo-bmkg.webp'); + background-size: contain; + background-repeat: no-repeat; + background-position: center; } .loader { - width: 48px; - height: 48px; - display: inline-block; - position: relative; + width: 48px; + height: 48px; + display: inline-block; + position: relative; } .loader::after, .loader::before { - content: ''; - box-sizing: border-box; - width: 48px; - height: 48px; - border: 2px solid var(--orange); - position: absolute; - left: 0; - top: 0; - animation: scaleOut 2s ease-in-out infinite; + content: ''; + box-sizing: border-box; + width: 48px; + height: 48px; + border: 2px solid var(--orange); + position: absolute; + left: 0; + top: 0; + animation: scaleOut 2s ease-in-out infinite; } .loader::after { - border-color: var(--red); - animation-delay: 1s; + border-color: var(--red); + animation-delay: 1s; } #loading-screen { - background-color: black; + background-color: black; } @keyframes scaleOut { - 0% { - transform: scale(0); - } + 0% { + transform: scale(0); + } - 100% { - transform: scale(1); - } + 100% { + transform: scale(1); + } } /* ========================= 16. CIRCLES ANIMATION ========================= */ .circles .circle1 { - animation-delay: 1s; + animation-delay: 1s; } .circles .circle2 { - animation-delay: 2s; + animation-delay: 2s; } .circles .circle3 { - animation-delay: 3s; + animation-delay: 3s; } .circles { - height: 200px; - width: 200px; - margin: auto; + height: 200px; + width: 200px; + margin: auto; } /* .circles div { @@ -996,155 +1029,182 @@ label#internal { } */ @keyframes growAndFade { - 0% { - opacity: .25; - transform: scale(0); - } + 0% { + opacity: 0.25; + transform: scale(0); + } - 100% { - opacity: 0; - transform: scale(1); - } + 100% { + opacity: 0; + transform: scale(1); + } } /* ========================= 17. HEXAGON BACKGROUND ========================= */ .main { - width: calc(max(120vh,120vw) + 100px); - margin-left: -35vh; - transform: translateY(min(-29vw,-40vw)); - display: grid; - grid-template-columns: repeat(auto-fit,calc(var(--s) + 2*var(--mh))); - justify-content:center; - --s: 80px; /* size */ - --r: 1.15; /* ratio */ - --h: 0.5; - --v: 0.25; - --hc:calc(clamp(0,var(--h),0.5) * var(--s)) ; - --vc:calc(clamp(0,var(--v),0.5) * var(--s) * var(--r)); - --mv: 1px; /* vertical */ - --mh: calc(var(--mv) + (var(--s) - 2*var(--hc))/2); /* horizontal */ - --f: calc(2*var(--s)*var(--r) + 4*var(--mv) - 2*var(--vc) - 2px); + width: calc(max(120vh, 120vw) + 100px); + margin-left: -35vh; + transform: translateY(min(-29vw, -40vw)); + display: grid; + grid-template-columns: repeat(auto-fit, calc(var(--s) + 2 * var(--mh))); + justify-content: center; + --s: 80px; /* size */ + --r: 1.15; /* ratio */ + --h: 0.5; + --v: 0.25; + --hc: calc(clamp(0, var(--h), 0.5) * var(--s)); + --vc: calc(clamp(0, var(--v), 0.5) * var(--s) * var(--r)); + --mv: 1px; /* vertical */ + --mh: calc(var(--mv) + (var(--s) - 2 * var(--hc)) / 2); /* horizontal */ + --f: calc(2 * var(--s) * var(--r) + 4 * var(--mv) - 2 * var(--vc) - 2px); } - + .hex-bg { - grid-column: 1/-1; - margin:0 auto; - font-size: 0; - position:relative; + grid-column: 1/-1; + margin: 0 auto; + font-size: 0; + position: relative; } .hex-bg div { - width: var(--s); - margin: var(--mv) var(--mh); - height: calc(var(--s)*var(--r)); - display: inline-block; - font-size:initial; - margin-bottom: calc(var(--mv) - var(--vc)); + width: var(--s); + margin: var(--mv) var(--mh); + height: calc(var(--s) * var(--r)); + display: inline-block; + font-size: initial; + margin-bottom: calc(var(--mv) - var(--vc)); } -.hex-bg::before{ - content: ""; - width: calc(var(--s)/2 + var(--mh)); - float: left; - height: 100%; - shape-outside: repeating-linear-gradient( - transparent 0 calc(var(--f) - 2px), - #fff 0 var(--f)); +.hex-bg::before { + content: ''; + width: calc(var(--s) / 2 + var(--mh)); + float: left; + height: 100%; + shape-outside: repeating-linear-gradient( + transparent 0 calc(var(--f) - 2px), + #fff 0 var(--f) + ); } .hex-bg div { - justify-content: center; - align-items: center; - font-weight:bold; - text-align:center; + justify-content: center; + align-items: center; + font-weight: bold; + text-align: center; } .hex-bg div p { - text-align: center; - margin-top: 20px; - color: black; - font-size: 10px; - transform: rotate(90deg); + text-align: center; + margin-top: 20px; + color: black; + font-size: 10px; + transform: rotate(90deg); } .hex-bg img { - display: block; - position: relative; - transform: rotate(90deg) scale(1.2); + display: block; + position: relative; + transform: rotate(90deg) scale(1.2); } .hex-bg div::before { - position:absolute; - display: flex; + position: absolute; + display: flex; } .hex-bg div { - animation:showPopUp 0.3s ease-in-out forwards; - opacity:0; - transform: scale(0.5); + animation: showPopUp 0.3s ease-in-out forwards; + opacity: 0; + transform: scale(0.5); } -@keyframes show{ - 10% { - opacity:1; - transform: scale(1); - } - 90% { - opacity:1; - transform: scale(1); - } +@keyframes show { + 10% { + opacity: 1; + transform: scale(1); + } + 90% { + opacity: 1; + transform: scale(1); + } } /* ========================= 18. MAPBOX OVERRIDES ========================= */ .mapbox-logo { - display: none !important; + display: none !important; } .mapboxgl-ctrl-logo { - display: none !important; + display: none !important; } .mapbox-gl-draw_point { - background-repeat: no-repeat; - background-position: center; - pointer-events: auto; - background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIgICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiICAgd2lkdGg9IjIwIiAgIGhlaWdodD0iMjAiICAgdmlld0JveD0iMCAwIDIwIDIwIiAgIGlkPSJzdmcxOTE2NyIgICB2ZXJzaW9uPSIxLjEiICAgaW5rc2NhcGU6dmVyc2lvbj0iMC45MStkZXZlbCtvc3htZW51IHIxMjkxMSIgICBzb2RpcG9kaTpkb2NuYW1lPSJtYXJrZXIuc3ZnIj4gIDxkZWZzICAgICBpZD0iZGVmczE5MTY5IiAvPiAgPHNvZGlwb2RpOm5hbWVkdmlldyAgICAgaWQ9ImJhc2UiICAgICBwYWdlY29sb3I9IiNmZmZmZmYiICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIgICAgIGJvcmRlcm9wYWNpdHk9IjEuMCIgICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIiAgICAgaW5rc2NhcGU6em9vbT0iMTYiICAgICBpbmtzY2FwZTpjeD0iMTQuMTY0MjUzIiAgICAgaW5rc2NhcGU6Y3k9IjguODg1NzIiICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJsYXllcjEiICAgICBzaG93Z3JpZD0iZmFsc2UiICAgICB1bml0cz0icHgiICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEyODAiICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSI3NTEiICAgICBpbmtzY2FwZTp3aW5kb3cteD0iMjA4IiAgICAgaW5rc2NhcGU6d2luZG93LXk9IjE5MCIgICAgIGlua3NjYXBlOndpbmRvdy1tYXhpbWl6ZWQ9IjAiICAgICBpbmtzY2FwZTpvYmplY3Qtbm9kZXM9InRydWUiPiAgICA8aW5rc2NhcGU6Z3JpZCAgICAgICB0eXBlPSJ4eWdyaWQiICAgICAgIGlkPSJncmlkMTk3MTUiIC8+ICA8L3NvZGlwb2RpOm5hbWVkdmlldz4gIDxtZXRhZGF0YSAgICAgaWQ9Im1ldGFkYXRhMTkxNzIiPiAgICA8cmRmOlJERj4gICAgICA8Y2M6V29yayAgICAgICAgIHJkZjphYm91dD0iIj4gICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PiAgICAgICAgPGRjOnR5cGUgICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+ICAgICAgICA8ZGM6dGl0bGUgLz4gICAgICA8L2NjOldvcms+ICAgIDwvcmRmOlJERj4gIDwvbWV0YWRhdGE+ICA8ZyAgICAgaW5rc2NhcGU6bGFiZWw9IkxheWVyIDEiICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIiAgICAgaWQ9ImxheWVyMSIgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsLTEwMzIuMzYyMikiPiAgICA8cGF0aCAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtjbGlwLXJ1bGU6bm9uemVybztkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO3Zpc2liaWxpdHk6dmlzaWJsZTtvcGFjaXR5OjE7aXNvbGF0aW9uOmF1dG87bWl4LWJsZW5kLW1vZGU6bm9ybWFsO2NvbG9yLWludGVycG9sYXRpb246c1JHQjtjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM6bGluZWFyUkdCO3NvbGlkLWNvbG9yOiMwMDAwMDA7c29saWQtb3BhY2l0eToxO2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MjtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDtzdHJva2Utb3BhY2l0eToxO21hcmtlcjpub25lO2NvbG9yLXJlbmRlcmluZzphdXRvO2ltYWdlLXJlbmRlcmluZzphdXRvO3NoYXBlLXJlbmRlcmluZzphdXRvO3RleHQtcmVuZGVyaW5nOmF1dG87ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIgICAgICAgZD0ibSAzNiwxMDQwLjM2MjIgYyA2ZS02LDMuMzA5MyAtNS45ODg2MTIsMTAgLTUuOTg4NjEyLDEwIDAsMCAtNS45OTg3NzYsLTYuNjY4IC02LjAxMTM0NSwtOS45NzcyIC0wLjAxMjU3LC0zLjMwOTIgMi42NTY1NzYsLTYuMDAzOSA1Ljk2NTc5MiwtNi4wMjI3IDMuMzA5MTg5LC0wLjAxOSA2LjAwODg0LDIuNjQ1MiA2LjAzMzk5Miw1Ljk1NDMiICAgICAgIGlkPSJwYXRoMTI1NjEiICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2Nzc2MiIC8+ICAgIDxwYXRoICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2NsaXAtcnVsZTpub256ZXJvO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7dmlzaWJpbGl0eTp2aXNpYmxlO29wYWNpdHk6MTtpc29sYXRpb246YXV0bzttaXgtYmxlbmQtbW9kZTpub3JtYWw7Y29sb3ItaW50ZXJwb2xhdGlvbjpzUkdCO2NvbG9yLWludGVycG9sYXRpb24tZmlsdGVyczpsaW5lYXJSR0I7c29saWQtY29sb3I6IzAwMDAwMDtzb2xpZC1vcGFjaXR5OjE7ZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZTpub25lO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO3N0cm9rZS1vcGFjaXR5OjE7bWFya2VyOm5vbmU7Y29sb3ItcmVuZGVyaW5nOmF1dG87aW1hZ2UtcmVuZGVyaW5nOmF1dG87c2hhcGUtcmVuZGVyaW5nOmF1dG87dGV4dC1yZW5kZXJpbmc6YXV0bztlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIiAgICAgICBkPSJtIDM0LjAwMDExNSwxMDQwLjM2MjIgYyAtNWUtNiwyLjIwNjIgLTMuOTkyNTIzLDcuMDAwMSAtMy45OTI1MjMsNy4wMDAxIDAsMCAtMy45OTkyOTEsLTQuNzc4NyAtNC4wMDc2NzksLTYuOTg0OSAtMC4wMDg0LC0yLjIwNjIgMS43NzEwODIsLTQuMDAyNyAzLjk3NzMxLC00LjAxNTMgMi4yMDYyMSwtMC4wMTMgNC4wMDYwMzcsMS43NjM1IDQuMDIyNzc3LDMuOTY5NyIgICAgICAgaWQ9InBhdGgxMjU2MyIgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2NzYyIgLz4gICAgPHBhdGggICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7Y2xpcC1ydWxlOm5vbnplcm87ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTt2aXNpYmlsaXR5OnZpc2libGU7b3BhY2l0eToxO2lzb2xhdGlvbjphdXRvO21peC1ibGVuZC1tb2RlOm5vcm1hbDtjb2xvci1pbnRlcnBvbGF0aW9uOnNSR0I7Y29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzOmxpbmVhclJHQjtzb2xpZC1jb2xvcjojMDAwMDAwO3NvbGlkLW9wYWNpdHk6MTtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7c3Ryb2tlLW9wYWNpdHk6MTttYXJrZXI6bm9uZTtjb2xvci1yZW5kZXJpbmc6YXV0bztpbWFnZS1yZW5kZXJpbmc6YXV0bztzaGFwZS1yZW5kZXJpbmc6YXV0bzt0ZXh0LXJlbmRlcmluZzphdXRvO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiICAgICAgIGQ9Ik0gOS45NjY3OTY5LDEwMTQuMzYyMiBDIDYuNjU3NTgwOSwxMDE0LjM4MSAzLjk4NzQzLDEwMTcuMDc2NCA0LDEwMjAuMzg1NiBjIDAuMDEyNTY5LDMuMzA5MiA2LjAxMTcxOSw4Ljk3NjYgNi4wMTE3MTksOC45NzY2IDAsMCA1Ljk4ODI4NywtNS42OTA3IDUuOTg4MjgxLC05IGwgMCwtMC4wNDUgYyAtMC4wMjUxNSwtMy4zMDkxIC0yLjcyNDAxNCwtNS45NzQxIC02LjAzMzIwMzEsLTUuOTU1MSB6IG0gMC4wMDk3NywyIGMgMi4yMDYyMDYxLC0wLjAxMyA0LjAwNjY5MzEsMS43NjI2IDQuMDIzNDMzMSwzLjk2ODggbCAwLDAuMDMxIGMgLTVlLTYsMi4yMDYyIC0zLjk5MjE4OCw2IC0zLjk5MjE4OCw2IDAsMCAtMy45OTk0MjQsLTMuNzc4MiAtNC4wMDc4MTIsLTUuOTg0NCAtMC4wMDg0LC0yLjIwNjIgMS43NzAzMzQ1LC00LjAwMyAzLjk3NjU2MjUsLTQuMDE1NiB6IiAgICAgICBpZD0icGF0aDEyNTY4IiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzY3NjY2Njc2NzYyIgLz4gICAgPHBhdGggICAgICAgc3R5bGU9Im9wYWNpdHk6MTtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46YmV2ZWw7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDtzdHJva2Utb3BhY2l0eToxO21hcmtlcjpub25lIiAgICAgICBkPSJNIDEwIDIgQyA2LjY4NjI5MiAyIDQgNC42ODYzIDQgOCBDIDQgMTEuMzEzNyAxMCAxNyAxMCAxNyBDIDEwIDE3IDE2IDExLjMxMzcgMTYgOCBDIDE2IDQuNjg2MyAxMy4zMTM3MDggMiAxMCAyIHogTSAxMCA0IEMgMTIuMDcxMDY4IDQgMTMuNzUgNS42Nzg5IDEzLjc1IDcuNzUgQyAxMy43NSA5LjIwNTMyNzggMTEuOTMxMTEgMTEuNjQ0MzkzIDEwLjgzMDA3OCAxMyBMIDkuMTY5OTIxOSAxMyBDIDguMDY4ODkwMyAxMS42NDQzOTMgNi4yNSA5LjIwNTMyNzggNi4yNSA3Ljc1IEMgNi4yNSA1LjY3ODkgNy45Mjg5MzIgNCAxMCA0IHogIiAgICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwLDEwMzIuMzYyMikiICAgICAgIGlkPSJwYXRoMTczMDUiIC8+ICA8L2c+PC9zdmc+); + background-repeat: no-repeat; + background-position: center; + pointer-events: auto; + background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIgICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiICAgd2lkdGg9IjIwIiAgIGhlaWdodD0iMjAiICAgdmlld0JveD0iMCAwIDIwIDIwIiAgIGlkPSJzdmcxOTE2NyIgICB2ZXJzaW9uPSIxLjEiICAgaW5rc2NhcGU6dmVyc2lvbj0iMC45MStkZXZlbCtvc3htZW51IHIxMjkxMSIgICBzb2RpcG9kaTpkb2NuYW1lPSJtYXJrZXIuc3ZnIj4gIDxkZWZzICAgICBpZD0iZGVmczE5MTY5IiAvPiAgPHNvZGlwb2RpOm5hbWVkdmlldyAgICAgaWQ9ImJhc2UiICAgICBwYWdlY29sb3I9IiNmZmZmZmYiICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIgICAgIGJvcmRlcm9wYWNpdHk9IjEuMCIgICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIiAgICAgaW5rc2NhcGU6em9vbT0iMTYiICAgICBpbmtzY2FwZTpjeD0iMTQuMTY0MjUzIiAgICAgaW5rc2NhcGU6Y3k9IjguODg1NzIiICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJsYXllcjEiICAgICBzaG93Z3JpZD0iZmFsc2UiICAgICB1bml0cz0icHgiICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEyODAiICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSI3NTEiICAgICBpbmtzY2FwZTp3aW5kb3cteD0iMjA4IiAgICAgaW5rc2NhcGU6d2luZG93LXk9IjE5MCIgICAgIGlua3NjYXBlOndpbmRvdy1tYXhpbWl6ZWQ9IjAiICAgICBpbmtzY2FwZTpvYmplY3Qtbm9kZXM9InRydWUiPiAgICA8aW5rc2NhcGU6Z3JpZCAgICAgICB0eXBlPSJ4eWdyaWQiICAgICAgIGlkPSJncmlkMTk3MTUiIC8+ICA8L3NvZGlwb2RpOm5hbWVkdmlldz4gIDxtZXRhZGF0YSAgICAgaWQ9Im1ldGFkYXRhMTkxNzIiPiAgICA8cmRmOlJERj4gICAgICA8Y2M6V29yayAgICAgICAgIHJkZjphYm91dD0iIj4gICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PiAgICAgICAgPGRjOnR5cGUgICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+ICAgICAgICA8ZGM6dGl0bGUgLz4gICAgICA8L2NjOldvcms+ICAgIDwvcmRmOlJERj4gIDwvbWV0YWRhdGE+ICA8ZyAgICAgaW5rc2NhcGU6bGFiZWw9IkxheWVyIDEiICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIiAgICAgaWQ9ImxheWVyMSIgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsLTEwMzIuMzYyMikiPiAgICA8cGF0aCAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtjbGlwLXJ1bGU6bm9uemVybztkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO3Zpc2liaWxpdHk6dmlzaWJsZTtvcGFjaXR5OjE7aXNvbGF0aW9uOmF1dG87bWl4LWJsZW5kLW1vZGU6bm9ybWFsO2NvbG9yLWludGVycG9sYXRpb246c1JHQjtjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM6bGluZWFyUkdCO3NvbGlkLWNvbG9yOiMwMDAwMDA7c29saWQtb3BhY2l0eToxO2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MjtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDtzdHJva2Utb3BhY2l0eToxO21hcmtlcjpub25lO2NvbG9yLXJlbmRlcmluZzphdXRvO2ltYWdlLXJlbmRlcmluZzphdXRvO3NoYXBlLXJlbmRlcmluZzphdXRvO3RleHQtcmVuZGVyaW5nOmF1dG87ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIgICAgICAgZD0ibSAzNiwxMDQwLjM2MjIgYyA2ZS02LDMuMzA5MyAtNS45ODg2MTIsMTAgLTUuOTg4NjEyLDEwIDAsMCAtNS45OTg3NzYsLTYuNjY4IC02LjAxMTM0NSwtOS45NzcyIC0wLjAxMjU3LC0zLjMwOTIgMi42NTY1NzYsLTYuMDAzOSA1Ljk2NTc5MiwtNi4wMjI3IDMuMzA5MTg5LC0wLjAxOSA2LjAwODg0LDIuNjQ1MiA2LjAzMzk5Miw1Ljk1NDMiICAgICAgIGlkPSJwYXRoMTI1NjEiICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2Nzc2MiIC8+ICAgIDxwYXRoICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2NsaXAtcnVsZTpub256ZXJvO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7dmlzaWJpbGl0eTp2aXNpYmxlO29wYWNpdHk6MTtpc29sYXRpb246YXV0bzttaXgtYmxlbmQtbW9kZTpub3JtYWw7Y29sb3ItaW50ZXJwb2xhdGlvbjpzUkdCO2NvbG9yLWludGVycG9sYXRpb24tZmlsdGVyczpsaW5lYXJSR0I7c29saWQtY29sb3I6IzAwMDAwMDtzb2xpZC1vcGFjaXR5OjE7ZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZTpub25lO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO3N0cm9rZS1vcGFjaXR5OjE7bWFya2VyOm5vbmU7Y29sb3ItcmVuZGVyaW5nOmF1dG87aW1hZ2UtcmVuZGVyaW5nOmF1dG87c2hhcGUtcmVuZGVyaW5nOmF1dG87dGV4dC1yZW5kZXJpbmc6YXV0bztlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIiAgICAgICBkPSJtIDM0LjAwMDExNSwxMDQwLjM2MjIgYyAtNWUtNiwyLjIwNjIgLTMuOTkyNTIzLDcuMDAwMSAtMy45OTI1MjMsNy4wMDAxIDAsMCAtMy45OTkyOTEsLTQuNzc4NyAtNC4wMDc2NzksLTYuOTg0OSAtMC4wMDg0LC0yLjIwNjIgMS43NzEwODIsLTQuMDAyNyAzLjk3NzMxLC00LjAxNTMgMi4yMDYyMSwtMC4wMTMgNC4wMDYwMzcsMS43NjM1IDQuMDIyNzc3LDMuOTY5NyIgICAgICAgaWQ9InBhdGgxMjU2MyIgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2NzYyIgLz4gICAgPHBhdGggICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7Y2xpcC1ydWxlOm5vbnplcm87ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTt2aXNpYmlsaXR5OnZpc2libGU7b3BhY2l0eToxO2lzb2xhdGlvbjphdXRvO21peC1ibGVuZC1tb2RlOm5vcm1hbDtjb2xvci1pbnRlcnBvbGF0aW9uOnNSR0I7Y29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzOmxpbmVhclJHQjtzb2xpZC1jb2xvcjojMDAwMDAwO3NvbGlkLW9wYWNpdHk6MTtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7c3Ryb2tlLW9wYWNpdHk6MTttYXJrZXI6bm9uZTtjb2xvci1yZW5kZXJpbmc6YXV0bztpbWFnZS1yZW5kZXJpbmc6YXV0bztzaGFwZS1yZW5kZXJpbmc6YXV0bzt0ZXh0LXJlbmRlcmluZzphdXRvO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiICAgICAgIGQ9Ik0gOS45NjY3OTY5LDEwMTQuMzYyMiBDIDYuNjU3NTgwOSwxMDE0LjM4MSAzLjk4NzQzLDEwMTcuMDc2NCA0LDEwMjAuMzg1NiBjIDAuMDEyNTY5LDMuMzA5MiA2LjAxMTcxOSw4Ljk3NjYgNi4wMTE3MTksOC45NzY2IDAsMCA1Ljk4ODI4NywtNS42OTA3IDUuOTg4MjgxLC05IGwgMCwtMC4wNDUgYyAtMC4wMjUxNSwtMy4zMDkxIC0yLjcyNDAxNCwtNS45NzQxIC02LjAzMzIwMzEsLTUuOTU1MSB6IG0gMC4wMDk3NywyIGMgMi4yMDYyMDYxLC0wLjAxMyA0LjAwNjY5MzEsMS43NjI2IDQuMDIzNDMzMSwzLjk2ODggbCAwLDAuMDMxIGMgLTVlLTYsMi4yMDYyIC0zLjk5MjE4OCw2IC0zLjk5MjE4OCw2IDAsMCAtMy45OTk0MjQsLTMuNzc4MiAtNC4wMDc4MTIsLTUuOTg0NCAtMC4wMDg0LC0yLjIwNjIgMS43NzAzMzQ1LC00LjAwMyAzLjk3NjU2MjUsLTQuMDE1NiB6IiAgICAgICBpZD0icGF0aDEyNTY4IiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzY3NjY2Njc2NzYyIgLz4gICAgPHBhdGggICAgICAgc3R5bGU9Im9wYWNpdHk6MTtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46YmV2ZWw7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDtzdHJva2Utb3BhY2l0eToxO21hcmtlcjpub25lIiAgICAgICBkPSJNIDEwIDIgQyA2LjY4NjI5MiAyIDQgNC42ODYzIDQgOCBDIDQgMTEuMzEzNyAxMCAxNyAxMCAxNyBDIDEwIDE3IDE2IDExLjMxMzcgMTYgOCBDIDE2IDQuNjg2MyAxMy4zMTM3MDggMiAxMCAyIHogTSAxMCA0IEMgMTIuMDcxMDY4IDQgMTMuNzUgNS42Nzg5IDEzLjc1IDcuNzUgQyAxMy43NSA5LjIwNTMyNzggMTEuOTMxMTEgMTEuNjQ0MzkzIDEwLjgzMDA3OCAxMyBMIDkuMTY5OTIxOSAxMyBDIDguMDY4ODkwMyAxMS42NDQzOTMgNi4yNSA5LjIwNTMyNzggNi4yNSA3Ljc1IEMgNi4yNSA1LjY3ODkgNy45Mjg5MzIgNCAxMCA0IHogIiAgICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwLDEwMzIuMzYyMikiICAgICAgIGlkPSJwYXRoMTczMDUiIC8+ICA8L2c+PC9zdmc+); } /* ========================= 19. DIGITAL CLOCK ========================= */ .digital-clock { - font-family: monospace; - font-size: 1rem; - font-weight: bold; - color: #ffb700; - background-color: #000; - padding: 0.25rem 0.5rem; - border-radius: 0.25rem; - border: 1px solid #333; - text-align: center; - letter-spacing: 0.05rem; - box-shadow: 0 0 5px rgba(255, 183, 0, 0.5); + font-family: monospace; + font-size: 1rem; + font-weight: bold; + color: #ffb700; + background-color: #000; + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + border: 1px solid #333; + text-align: center; + letter-spacing: 0.05rem; + box-shadow: 0 0 5px rgba(255, 183, 0, 0.5); } /* ========================= 20. MAPBOX CONTAINER & FONT ========================= */ .mapbox-container { - margin: 0; - padding: 0; - font-family: 'Roboto Condensed', Arial, Helvetica, sans-serif; + margin: 0; + padding: 0; + font-family: 'Roboto Condensed', Arial, Helvetica, sans-serif; } .mapboxgl-map { - font-family: 'Roboto Condensed', Arial, Helvetica, sans-serif; + font-family: 'Roboto Condensed', Arial, Helvetica, sans-serif; } +/* ========================= + 21. RANDOM + ========================= */ + +.animate-ping { + animation: ping 1.5s cubic-bezier(0, 0, 0.2, 1) infinite; +} + +@keyframes ping { + 0% { + transform: translate(-50%, 100%) scale(0.5); + opacity: 1; + } + 75%, + 100% { + transform: translate(-50%, 100%) scale(2); + opacity: 0; + } +} + +/* .incident-logs-popup .mapboxgl-popup-content { + padding: 0; + overflow: hidden; + border-radius: 0.5rem; +} */ + /* ========================================================== END OF SIGAP UI CSS ========================================================== diff --git a/sigap-website/app/_utils/types/crimes.ts b/sigap-website/app/_utils/types/crimes.ts index 7f6466b..3e60ceb 100644 --- a/sigap-website/app/_utils/types/crimes.ts +++ b/sigap-website/app/_utils/types/crimes.ts @@ -97,16 +97,21 @@ export interface IDistanceResult { export interface IIncidentLogs { id: string; user_id: string; - latitude: number; - longitude: number; - district: string; - address: string; - category: string; - source: string; + role_id: string; + name: string; + email: string; + phone: string; + avatar: string; + role: string; description: string; verified: boolean; - severity: "Low" | "Medium" | "High" | "Unknown"; + longitude: number; + latitude: number; timestamp: Date; - created_at: Date; - updated_at: Date; -} \ No newline at end of file + category: string; + address: string; + district: string; + severity: "Low" | "Medium" | "High" | "Unknown"; + source: string; + isVeryRecent?: boolean; +} diff --git a/sigap-website/prisma/backups/function.sql b/sigap-website/prisma/backups/function.sql new file mode 100644 index 0000000..2e66206 --- /dev/null +++ b/sigap-website/prisma/backups/function.sql @@ -0,0 +1,101 @@ +create or replace function public.nearby_units( + lat double precision, + lon double precision, + max_results integer default 5 +) +returns table ( + code_unit varchar, + name text, + type text, + address text, + district_id varchar, + lat_unit double precision, + lon_unit double precision, + distance_km double precision +) +language sql +as $$ + select + u.code_unit, + u.name, + u.type, + u.address, + u.district_id, + gis.ST_Y(u.location::gis.geometry) as lat_unit, + gis.ST_X(u.location::gis.geometry) as lon_unit, + gis.ST_Distance( + u.location::gis.geography, + gis.ST_SetSRID(gis.ST_MakePoint(lon, lat), 4326)::gis.geography + ) / 1000 as distance_km + from units u + order by gis.ST_Distance( + u.location::gis.geography, + gis.ST_SetSRID(gis.ST_MakePoint(lon, lat), 4326)::gis.geography + ) + limit max_results +$$; + + +CREATE OR REPLACE FUNCTION public.update_location_distance_to_unit() +RETURNS TRIGGER AS $$ +DECLARE + loc_lat FLOAT; + loc_lng FLOAT; + unit_lat FLOAT; + unit_lng FLOAT; + loc_point GEOGRAPHY; + unit_point GEOGRAPHY; +BEGIN + -- Ambil lat/lng dari location yang baru + SELECT gis.ST_Y(NEW.location::gis.geometry), gis.ST_X(NEW.location::gis.geometry) + INTO loc_lat, loc_lng; + + -- Ambil lat/lng dari unit di distrik yang sama + SELECT gis.ST_Y(u.location::gis.geometry), gis.ST_X(u.location::gis.geometry) + INTO unit_lat, unit_lng + FROM units u + WHERE u.district_id = NEW.district_id + LIMIT 1; + + -- Jika tidak ada unit di distrik yang sama, kembalikan NEW tanpa perubahan + IF unit_lat IS NULL OR unit_lng IS NULL THEN + RETURN NEW; + END IF; + + -- Buat point geography dari lat/lng + loc_point := gis.ST_SetSRID(gis.ST_MakePoint(loc_lng, loc_lat), 4326)::gis.geography; + unit_point := gis.ST_SetSRID(gis.ST_MakePoint(unit_lng, unit_lat), 4326)::gis.geography; + + -- Update jaraknya ke kolom distance_to_unit + NEW.distance_to_unit := gis.ST_Distance(loc_point, unit_point) / 1000; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create the trigger +CREATE OR REPLACE TRIGGER update_location_distance_trigger +BEFORE INSERT OR UPDATE OF location, district_id +ON locations +FOR EACH ROW +EXECUTE FUNCTION public.update_location_distance_to_unit(); + + +-- Spatial index untuk tabel units +CREATE INDEX IF NOT EXISTS idx_units_location_gist ON units USING GIST (location); + +-- Spatial index untuk tabel locations +CREATE INDEX IF NOT EXISTS idx_locations_location_gist ON locations USING GIST (location); + +-- Index untuk mempercepat pencarian units berdasarkan district_id +CREATE INDEX IF NOT EXISTS idx_units_district_id ON units (district_id); + +-- Index untuk mempercepat pencarian locations berdasarkan district_id +CREATE INDEX IF NOT EXISTS idx_locations_district_id ON locations (district_id); + +-- Index untuk kombinasi location dan district_id pada tabel units +CREATE INDEX IF NOT EXISTS idx_units_location_district ON units (district_id, location); + +-- Analisis tabel setelah membuat index +ANALYZE units; +ANALYZE locations; \ No newline at end of file