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)} + > + + + )}