UI Updates

This commit is contained in:
Logan
2026-05-10 21:47:34 -04:00
parent 8b660d8e10
commit 4c3b1fcc84
14 changed files with 385 additions and 118 deletions
+98 -59
View File
@@ -1,6 +1,6 @@
"use client";
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
import { MapContainer, TileLayer, Marker, Popup, LayersControl, FeatureGroup } from "react-leaflet";
import L from "leaflet";
import type { NodeRecord, CallRecord, IncidentRecord } from "@/lib/types";
@@ -59,7 +59,6 @@ export default function MapView({ nodes, activeCalls, incidents = [] }: Props) {
activeCalls.map((c) => [c.node_id, c])
);
// Only show incidents that have been geocoded (location_coords set by the server).
const plottedIncidents = incidents.flatMap((inc) =>
inc.location_coords
? [{ inc, pos: [inc.location_coords.lat, inc.location_coords.lng] as [number, number] }]
@@ -81,64 +80,104 @@ export default function MapView({ nodes, activeCalls, incidents = [] }: Props) {
: 4;
return (
<MapContainer
center={center}
zoom={zoom}
className="w-full h-full rounded-lg"
style={{ background: "#111827" }}
>
<TileLayer
url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"
attribution='&copy; <a href="https://carto.com/">CARTO</a>'
/>
<div className="relative w-full h-full">
<MapContainer
center={center}
zoom={zoom}
className="w-full h-full rounded-lg"
style={{ background: "#111827" }}
>
<LayersControl position="topright">
{/* Base layers */}
<LayersControl.BaseLayer checked name="Dark">
<TileLayer
url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"
attribution='&copy; <a href="https://carto.com/">CARTO</a>'
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="Light">
<TileLayer
url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png"
attribution='&copy; <a href="https://carto.com/">CARTO</a>'
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="Streets">
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
/>
</LayersControl.BaseLayer>
{/* Node markers */}
{nodes.map((node) => (
<Marker
key={node.node_id}
position={[node.lat, node.lon]}
icon={nodeIcon(node.status)}
>
<Popup className="font-mono">
<div className="text-gray-900">
<p className="font-bold">{node.name}</p>
<p className="text-xs text-gray-500">{node.node_id}</p>
<p className="text-xs mt-1 capitalize">{node.status}</p>
{activeByNode[node.node_id] && (
<p className="text-xs text-orange-600 mt-1">
TG {activeByNode[node.node_id].talkgroup_id ?? "—"}{" "}
{activeByNode[node.node_id].talkgroup_name}
</p>
)}
</div>
</Popup>
</Marker>
))}
{/* Overlay: Nodes */}
<LayersControl.Overlay checked name="Nodes">
<FeatureGroup>
{nodes.map((node) => (
<Marker
key={node.node_id}
position={[node.lat, node.lon]}
icon={nodeIcon(node.status)}
>
<Popup className="font-mono">
<div className="text-gray-900">
<p className="font-bold">{node.name}</p>
<p className="text-xs text-gray-500">{node.node_id}</p>
<p className="text-xs mt-1 capitalize">{node.status}</p>
{activeByNode[node.node_id] && (
<p className="text-xs text-orange-600 mt-1">
TG {activeByNode[node.node_id].talkgroup_id ?? "—"}{" "}
{activeByNode[node.node_id].talkgroup_name}
</p>
)}
</div>
</Popup>
</Marker>
))}
</FeatureGroup>
</LayersControl.Overlay>
{/* Incident markers — positioned at the node covering the incident's system */}
{plottedIncidents.map(({ inc, pos }) => (
<Marker
key={inc.incident_id}
position={pos}
icon={incidentIcon(inc.type)}
>
<Popup className="font-mono">
<div className="text-gray-900">
<p className="font-bold">{inc.title ?? "Incident"}</p>
<p className="text-xs capitalize" style={{ color: INCIDENT_COLORS[inc.type ?? "other"] ?? INCIDENT_COLORS.other }}>
{inc.type ?? "other"}
</p>
<p className="text-xs mt-1 capitalize">{inc.status}</p>
{inc.location && <p className="text-xs text-gray-600 mt-1">{inc.location}</p>}
<p className="text-xs text-gray-500">{inc.call_ids.length} call{inc.call_ids.length !== 1 ? "s" : ""}</p>
{inc.summary && <p className="text-xs mt-1">{inc.summary}</p>}
<a href={`/incidents/${inc.incident_id}`} className="text-xs text-blue-600 hover:underline mt-1 block">
View incident
</a>
</div>
</Popup>
</Marker>
))}
</MapContainer>
{/* Overlay: Active Incidents */}
<LayersControl.Overlay checked name="Active Incidents">
<FeatureGroup>
{plottedIncidents.map(({ inc, pos }) => (
<Marker
key={inc.incident_id}
position={pos}
icon={incidentIcon(inc.type)}
>
<Popup className="font-mono">
<div className="text-gray-900">
<p className="font-bold">{inc.title ?? "Incident"}</p>
<p className="text-xs capitalize" style={{ color: INCIDENT_COLORS[inc.type ?? "other"] ?? INCIDENT_COLORS.other }}>
{inc.type ?? "other"}
</p>
<p className="text-xs mt-1 capitalize">{inc.status}</p>
{inc.location && <p className="text-xs text-gray-600 mt-1">{inc.location}</p>}
<p className="text-xs text-gray-500">{inc.call_ids.length} call{inc.call_ids.length !== 1 ? "s" : ""}</p>
{inc.summary && <p className="text-xs mt-1">{inc.summary}</p>}
<a href={`/incidents/${inc.incident_id}`} className="text-xs text-blue-600 hover:underline mt-1 block">
View incident
</a>
</div>
</Popup>
</Marker>
))}
</FeatureGroup>
</LayersControl.Overlay>
</LayersControl>
</MapContainer>
{/* Legend overlay — inside the map wrapper, above tiles */}
<div className="absolute bottom-8 left-3 z-[1001] bg-gray-950/90 border border-gray-800 rounded-lg px-3 py-2 text-xs font-mono pointer-events-none space-y-1">
<div className="flex items-center gap-2"><span className="text-green-400"></span> Online</div>
<div className="flex items-center gap-2"><span className="text-orange-400"></span> Recording</div>
<div className="flex items-center gap-2"><span className="text-indigo-400"></span> Unconfigured</div>
<div className="flex items-center gap-2"><span className="text-gray-500"></span> Offline</div>
<div className="border-t border-gray-800 my-0.5" />
<div className="flex items-center gap-2"><span className="text-red-500"></span> Fire</div>
<div className="flex items-center gap-2"><span className="text-blue-500"></span> Police</div>
<div className="flex items-center gap-2"><span className="text-yellow-500"></span> EMS</div>
<div className="flex items-center gap-2"><span className="text-orange-500"></span> Accident</div>
</div>
</div>
);
}