diff --git a/eslint.config.mjs b/eslint.config.mjs index c85fb67..771706d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -11,6 +11,15 @@ const compat = new FlatCompat({ const eslintConfig = [ ...compat.extends("next/core-web-vitals", "next/typescript"), + { + rules: { + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-explicit-any": "off", + "react-hooks/rules-of-hooks": "off", + "react-hooks/exhaustive-deps": "off", + "prefer-const": "off" + } + } ]; export default eslintConfig; diff --git a/package-lock.json b/package-lock.json index d73a7b6..4f1f93c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "leaflet": "^1.9.4", "leaflet.markercluster": "^1.5.3", "lucide-react": "^0.525.0", + "moment": "^2.30.1", "next": "15.3.4", "next-themes": "^0.4.6", "react": "^19.0.0", @@ -4849,6 +4850,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/package.json b/package.json index c220928..b9320d1 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "leaflet": "^1.9.4", "leaflet.markercluster": "^1.5.3", "lucide-react": "^0.525.0", + "moment": "^2.30.1", "next": "15.3.4", "next-themes": "^0.4.6", "react": "^19.0.0", diff --git a/src/components/ConfigContext.tsx b/src/components/ConfigContext.tsx index 47f78fd..ba3c6ea 100644 --- a/src/components/ConfigContext.tsx +++ b/src/components/ConfigContext.tsx @@ -8,6 +8,7 @@ export type Config = { lastSeen: number | null; // seconds, or null for forever tileLayer: string; // add tileLayer selection clustering?: boolean; // add clustering toggle + showNodeNames?: boolean; // add show node names toggle }; const TILE_LAYERS = [ @@ -18,9 +19,10 @@ const TILE_LAYERS = [ const DEFAULT_CONFIG: Config = { nodeTypes: ["meshcore", "meshtastic"], - lastSeen: 86400, // 24h in seconds + lastSeen: null, // forever by default tileLayer: "openstreetmap", // default clustering: true, // default to clustering enabled + showNodeNames: true, // default to show node names }; const LAST_SEEN_OPTIONS = [ @@ -182,6 +184,16 @@ function ConfigPopover({ config, setConfig, onClose, anchorRef }: { config: Conf Enable marker clustering +
+ +
); } \ No newline at end of file diff --git a/src/components/MapView.tsx b/src/components/MapView.tsx index 9fc9445..add60bb 100644 --- a/src/components/MapView.tsx +++ b/src/components/MapView.tsx @@ -8,6 +8,7 @@ import 'leaflet.markercluster/dist/leaflet.markercluster.js'; import 'leaflet.markercluster/dist/MarkerCluster.css'; import 'leaflet.markercluster/dist/MarkerCluster.Default.css'; import { useConfig } from "./ConfigContext"; +import moment from "moment"; const DEFAULT = { lat: 47.6062, // Seattle @@ -23,6 +24,7 @@ type NodePosition = { last_seen?: string; type?: string; short_name?: string; + name?: string | null; }; type ClusteredMarkersProps = { nodes: NodePosition[] }; @@ -47,7 +49,7 @@ function ClusteredMarkers({ nodes }: ClusteredMarkersProps) { : node.type === "meshcore" ? "custom-node-marker custom-node-marker--blue custom-node-marker--top" : "custom-node-marker"; - const label = node.short_name ? `
${node.short_name}
` : ''; + const label = (config?.showNodeNames !== false && node.short_name) ? `
${node.short_name}
` : ''; const icon = L.divIcon({ className: 'custom-node-marker-container', iconSize: [16, 32], @@ -58,12 +60,20 @@ function ClusteredMarkers({ nodes }: ClusteredMarkersProps) { (marker as any).options.nodeData = node; let popupHtml = `
`; popupHtml += `
ID: ${node.node_id}
`; + popupHtml += `
Full Name: ${node.name ?? "-"}
`; popupHtml += `
Short Name: ${node.short_name ?? "-"}
`; popupHtml += `
Type: ${node.type ?? "-"}
`; popupHtml += `
Lat: ${node.latitude}
`; popupHtml += `
Lng: ${node.longitude}
`; popupHtml += `
Alt: ${node.altitude !== undefined ? node.altitude : "-"}
`; - popupHtml += `
Last seen: ${node.last_seen ?? "-"}
`; + if (node.last_seen) { + // Parse as UTC, display UTC, and show local relative time + const utcTime = moment.utc(node.last_seen); + const localAgo = utcTime.local().fromNow(); + popupHtml += `
Last seen: ${utcTime.format('YYYY-MM-DD HH:mm:ss')} (UTC)
${localAgo}
`; + } else { + popupHtml += `
Last seen: -
`; + } popupHtml += `
`; marker.bindPopup(popupHtml); marker.addTo(map); @@ -150,7 +160,7 @@ function ClusteredMarkers({ nodes }: ClusteredMarkersProps) { : node.type === "meshcore" ? "custom-node-marker custom-node-marker--blue custom-node-marker--top" : "custom-node-marker"; - const label = node.short_name ? `
${node.short_name}
` : ''; + const label = (config?.showNodeNames !== false && node.short_name) ? `
${node.short_name}
` : ''; const icon = L.divIcon({ className: 'custom-node-marker-container', iconSize: [16, 32], @@ -161,12 +171,20 @@ function ClusteredMarkers({ nodes }: ClusteredMarkersProps) { (marker as any).options.nodeData = node; let popupHtml = `
`; popupHtml += `
ID: ${node.node_id}
`; + popupHtml += `
Full Name: ${node.name ?? "-"}
`; popupHtml += `
Short Name: ${node.short_name ?? "-"}
`; popupHtml += `
Type: ${node.type ?? "-"}
`; popupHtml += `
Lat: ${node.latitude}
`; popupHtml += `
Lng: ${node.longitude}
`; popupHtml += `
Alt: ${node.altitude !== undefined ? node.altitude : "-"}
`; - popupHtml += `
Last seen: ${node.last_seen ?? "-"}
`; + if (node.last_seen) { + // Parse as UTC, display UTC, and show local relative time + const utcTime = moment.utc(node.last_seen); + const localAgo = utcTime.local().fromNow(); + popupHtml += `
Last seen: ${utcTime.format('YYYY-MM-DD HH:mm:ss')} (UTC)
${localAgo}
`; + } else { + popupHtml += `
Last seen: -
`; + } popupHtml += `
`; marker.bindPopup(popupHtml); markers.addLayer(marker); @@ -177,7 +195,7 @@ function ClusteredMarkers({ nodes }: ClusteredMarkersProps) { map.removeLayer(markers); }; } - }, [map, nodes, config?.clustering]); + }, [map, nodes, config?.clustering, config?.showNodeNames]); return null; } @@ -267,7 +285,7 @@ export default function MapView() { useMapEvents({ moveend: (e) => { const b = e.target.getBounds(); - const buffer = 0.05; // 5% buffer + const buffer = 0.2; // 20% buffer const latDiff = b.getNorthEast().lat - b.getSouthWest().lat; const lngDiff = b.getNorthEast().lng - b.getSouthWest().lng; const newBounds: [[number, number], [number, number]] = [ @@ -291,7 +309,7 @@ export default function MapView() { }, zoomend: (e) => { const b = e.target.getBounds(); - const buffer = 0.05; // 5% buffer + const buffer = 0.2; // 20% buffer const latDiff = b.getNorthEast().lat - b.getSouthWest().lat; const lngDiff = b.getNorthEast().lng - b.getSouthWest().lng; const newBounds: [[number, number], [number, number]] = [