diff --git a/src/components/PageComponents/Map/NodeDetail.tsx b/src/components/PageComponents/Map/NodeDetail.tsx
new file mode 100644
index 00000000..cc01b8db
--- /dev/null
+++ b/src/components/PageComponents/Map/NodeDetail.tsx
@@ -0,0 +1,171 @@
+import { Mono } from "@components/generic/Mono.tsx";
+import { H5 } from "@app/components/UI/Typography/H5.tsx";
+import { Subtle } from "@app/components/UI/Typography/Subtle.tsx";
+import { Separator } from "@app/components/UI/Seperator";
+import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.tsx";
+import { Hashicon } from "@emeraldpay/hashicon-react";
+import { Protobuf } from "@meshtastic/js";
+import type { Protobuf as ProtobufType } from "@meshtastic/js";
+import {
+ BatteryChargingIcon,
+ BatteryFullIcon,
+ BatteryLowIcon,
+ BatteryMediumIcon,
+ Dot,
+ LockIcon,
+ LockOpenIcon,
+ MountainSnow,
+ Star,
+} from "lucide-react";
+import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
+
+export interface NodeDetailProps {
+ node: ProtobufType.Mesh.NodeInfo;
+}
+
+export const NodeDetail = ({ node }: NodeDetailProps): JSX.Element => {
+ const name = node.user?.longName || `!${numberToHexUnpadded(node.num)}`;
+ const hardwareType = Protobuf.Mesh.HardwareModel[
+ node.user?.hwModel ?? 0
+ ].replaceAll("_", " ");
+
+ return (
+ <>
+
+
+
+
+
+ {node.user?.publicKey && node.user?.publicKey.length > 0 ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
{name}
+
+ {hardwareType !== "UNSET" &&
{hardwareType}}
+
+ {!!node.deviceMetrics?.batteryLevel && (
+
+ {node.deviceMetrics?.batteryLevel > 100 ? (
+
+ ) : node.deviceMetrics?.batteryLevel > 80 ? (
+
+ ) : node.deviceMetrics?.batteryLevel > 20 ? (
+
+ ) : (
+
+ )}
+
+ {node.deviceMetrics?.batteryLevel > 100
+ ? "Charging"
+ : node.deviceMetrics?.batteryLevel + "%"}
+
+
+ )}
+
+
+ {node.user?.shortName &&
"{node.user?.shortName}"
}
+ {node.user?.id &&
{node.user?.id}
}
+
+
+
+
+ {node.lastHeard > 0 && (
+
+ Heard
+
+ )}
+
+ {node.viaMqtt && (
+
+ MQTT
+
+ )}
+
+
+
+
+
+
+
+
+
+ {isNaN(node.hopsAway) ? "?" : node.hopsAway}
+
+
{node.hopsAway === 1 ? "Hop" : "Hops"}
+
+ {node.position?.altitude && (
+
+
+
{node.position?.altitude} ft
+
+ )}
+
+
+
+ {!!node.deviceMetrics?.channelUtilization && (
+
+
Channel Util
+
+ {node.deviceMetrics?.channelUtilization.toPrecision(3)}%
+
+
+ )}
+ {!!node.deviceMetrics?.airUtilTx && (
+
+
Airtime Util
+
{node.deviceMetrics?.airUtilTx.toPrecision(3)}%
+
+ )}
+
+
+ {node.snr !== 0 && (
+
+
SNR
+
+ {node.snr}db
+
+ {Math.min(Math.max((node.snr + 10) * 5, 0), 100)}%
+
+ {(node.snr + 10) * 5}raw
+
+
+ )}
+ >
+ );
+};
diff --git a/src/components/UI/Typography/H5.tsx b/src/components/UI/Typography/H5.tsx
new file mode 100644
index 00000000..5bd1dfb1
--- /dev/null
+++ b/src/components/UI/Typography/H5.tsx
@@ -0,0 +1,14 @@
+import { cn } from "@app/core/utils/cn.ts";
+
+export interface H5Props {
+ className?: string;
+ children: React.ReactNode;
+}
+
+export const H5 = ({ className, children }: H5Props): JSX.Element => (
+
+ {children}
+
+);
diff --git a/src/pages/Map.tsx b/src/pages/Map.tsx
index 1deeee64..e326b336 100644
--- a/src/pages/Map.tsx
+++ b/src/pages/Map.tsx
@@ -1,4 +1,5 @@
import { Subtle } from "@app/components/UI/Typography/Subtle.tsx";
+import { NodeDetail } from "@app/components/PageComponents/Map/NodeDetail";
import { cn } from "@app/core/utils/cn.ts";
import { PageLayout } from "@components/PageLayout.tsx";
import { Sidebar } from "@components/Sidebar.tsx";
@@ -16,8 +17,9 @@ import {
ZoomOutIcon,
} from "lucide-react";
import { useCallback, useEffect, useState } from "react";
-import { AttributionControl, Marker, useMap } from "react-map-gl";
+import { AttributionControl, Marker, Popup, useMap } from "react-map-gl";
import MapGl from "react-map-gl/maplibre";
+import { Protobuf } from "@meshtastic/js";
export const MapPage = (): JSX.Element => {
const { nodes, waypoints } = useDevice();
@@ -25,6 +27,8 @@ export const MapPage = (): JSX.Element => {
const { default: map } = useMap();
const [zoom, setZoom] = useState(0);
+ const [selectedNode, setSelectedNode] =
+ useState(null);
const allNodes = Array.from(nodes.values());
@@ -164,7 +168,7 @@ export const MapPage = (): JSX.Element => {
))} */}
{allNodes.map((node) => {
- if (node.position?.latitudeI) {
+ if (node.position?.latitudeI && node.num !== selectedNode?.num) {
return (
{
style={{ filter: darkMode ? "invert(1)" : "" }}
anchor="bottom"
onClick={() => {
+ setSelectedNode(node);
map?.easeTo({
zoom: 12,
center: [
@@ -193,6 +198,17 @@ export const MapPage = (): JSX.Element => {
);
}
})}
+ {selectedNode?.position && (
+ setSelectedNode(null)}
+ >
+
+
+ )}
>