feat: map overhaul, kiosk mode, RR importer, duplicate system
Map (MapView.tsx): - Fan/hand-of-cards marker clustering: groups nearby markers by pixel proximity (union-find), renders as rotated color cards showing all types - Pulsing ring CSS animation on recording nodes (pulse-ring keyframe) - Live incident overlay panel — right sidebar (desktop) / bottom drawer (mobile), clickable to flyTo incident location - Auto-fit button (⤢) fits all markers in view with fitBounds - "Live · Xs ago" timestamp badge (refreshes every 10s) - Weather Radar layer (NEXRAD via Iowa Env Mesonet, no API key) - ADS-B + Meshtastic placeholder layers (off by default) Map page (map/page.tsx): - Fullscreen / kiosk toggle: fixed z-50 overlay covers nav, map fills viewport - lastUpdated tracking passed to MapView for Live timestamp Systems page (systems/page.tsx): - Duplicate System button: opens form pre-filled with Copy of <name> - RadioReference HTML import: file upload → DOMParser validates .rrlblue structure, parses talkgroup categories, modal lets user select which categories to import, auto-maps RR tags to local tags (law→police, etc.)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
import { useNodes } from "@/lib/useNodes";
|
||||
@@ -45,12 +46,55 @@ function IncidentCard({ incident }: { incident: IncidentRecord }) {
|
||||
|
||||
export default function MapPage() {
|
||||
const { nodes, loading } = useNodes();
|
||||
const activeCalls = useActiveCalls();
|
||||
const incidents = useActiveIncidents();
|
||||
const activeCalls = useActiveCalls();
|
||||
const incidents = useActiveIncidents();
|
||||
const [kiosk, setKiosk] = useState(false);
|
||||
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
|
||||
|
||||
// Track when data last refreshed
|
||||
useEffect(() => {
|
||||
if (!loading) setLastUpdated(new Date());
|
||||
}, [nodes, activeCalls, incidents, loading]);
|
||||
|
||||
// Kiosk mode: full-viewport fixed overlay sits above the sticky nav (z-40 → z-50)
|
||||
if (kiosk) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 bg-gray-950">
|
||||
<MapView
|
||||
nodes={nodes}
|
||||
activeCalls={activeCalls}
|
||||
incidents={incidents}
|
||||
lastUpdated={lastUpdated}
|
||||
/>
|
||||
<button
|
||||
onClick={() => setKiosk(false)}
|
||||
title="Exit fullscreen"
|
||||
className="absolute top-3 left-3 z-[1002] bg-gray-950/90 border border-gray-700 rounded px-3 py-1.5 text-xs font-mono text-gray-300 hover:text-white hover:border-gray-500 transition-colors flex items-center gap-1.5"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
||||
<path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/>
|
||||
</svg>
|
||||
Exit fullscreen
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-xl font-bold text-white font-mono">Map</h1>
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-bold text-white font-mono">Map</h1>
|
||||
<button
|
||||
onClick={() => setKiosk(true)}
|
||||
title="Fullscreen / kiosk mode"
|
||||
className="text-xs font-mono text-gray-500 hover:text-gray-300 transition-colors flex items-center gap-1.5"
|
||||
>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
||||
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
|
||||
</svg>
|
||||
Fullscreen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-96 text-gray-600 font-mono text-sm">
|
||||
@@ -58,11 +102,15 @@ export default function MapPage() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-[50vh] sm:h-[65vh] min-h-[400px]">
|
||||
<MapView nodes={nodes} activeCalls={activeCalls} incidents={incidents} />
|
||||
<MapView
|
||||
nodes={nodes}
|
||||
activeCalls={activeCalls}
|
||||
incidents={incidents}
|
||||
lastUpdated={lastUpdated}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Active incidents — shown even without geocoded location */}
|
||||
{incidents.length > 0 && (
|
||||
<section>
|
||||
<h2 className="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3">
|
||||
|
||||
Reference in New Issue
Block a user