e55412d8c7
app/map/page.tsx
Removed IncidentCard component and the incidents grid below the map — the on-map sidebar inside MapView is the single display
Moved kiosk exit button from top-3 left-3 (overlapping zoom controls) to bottom-[5.5rem] left-3
components/MapView.tsx
Fixed popup "View incident →" link — adds stopPropagation() + window.location.href to prevent Leaflet intercepting the click
Added "View details →" link on each sidebar incident card so you can navigate from the map panel without opening a popup
Added "News Alerts" overlay layer (placeholder, ready for RSS/feed integration)
lib/types.ts
Added preferred_token_id?: string | null to SystemRecord
lib/c2api.ts
Added setPreferredToken(tokenId, systemId) calling PUT /tokens/{tokenId}/prefer/{systemId} (backend already existed)
app/systems/page.tsx
Added PreferredTokenPanel component — loads the token pool lazily on expand, shows radio buttons to set/clear the preferred token, displayed on each system card above the AI flags panel
81 lines
2.9 KiB
TypeScript
81 lines
2.9 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import dynamic from "next/dynamic";
|
|
import { useNodes } from "@/lib/useNodes";
|
|
import { useActiveCalls } from "@/lib/useCalls";
|
|
import { useActiveIncidents } from "@/lib/useIncidents";
|
|
|
|
const MapView = dynamic(() => import("@/components/MapView"), { ssr: false });
|
|
|
|
export default function MapPage() {
|
|
const { nodes, loading } = useNodes();
|
|
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 bottom-[5.5rem] 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">
|
|
<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">
|
|
Loading map…
|
|
</div>
|
|
) : (
|
|
<div className="h-[50vh] sm:h-[65vh] min-h-[400px]">
|
|
<MapView
|
|
nodes={nodes}
|
|
activeCalls={activeCalls}
|
|
incidents={incidents}
|
|
lastUpdated={lastUpdated}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
);
|
|
}
|