import type React from 'react'; import { useEffect, useState } from 'react'; import { m } from 'framer-motion'; import { LngLat } from 'mapbox-gl'; import { BiCrown } from 'react-icons/bi'; import { FiAlignLeft, FiCode, FiMapPin, FiSliders, FiUser, } from 'react-icons/fi'; import { IoTelescope } from 'react-icons/io5'; import { MdGpsFixed, MdGpsNotFixed, MdGpsOff } from 'react-icons/md'; import JSONPretty from 'react-json-pretty'; import { IconButton } from '@components/generic/button/IconButton'; import { CollapsibleSection } from '@components/generic/Sidebar/CollapsibleSection'; import { SidebarOverlay } from '@components/generic/Sidebar/SidebarOverlay'; import { Tooltip } from '@components/generic/Tooltip'; import { SidebarItem } from '@components/layout/Sidebar/SidebarItem'; import { CopyButton } from '@components/menu/buttons/CopyButton'; import { Hashicon } from '@emeraldpay/hashicon-react'; import { useMapbox } from '@hooks/useMapbox'; import type { Protobuf } from '@meshtastic/meshtasticjs'; type PositionConfidence = 'high' | 'low' | 'none'; export interface NodeCardProps { node: Protobuf.NodeInfo; isMyNode: boolean; selected: boolean; setSelected: () => void; } export const NodeCard = ({ node, isMyNode, selected, setSelected, }: NodeCardProps): JSX.Element => { const { map } = useMapbox(); const [infoOpen, setInfoOpen] = useState(false); const [PositionConfidence, setPositionConfidence] = useState<PositionConfidence>('none'); useEffect(() => { setPositionConfidence( node.position ? new Date(node.position.posTimestamp * 1000) > new Date(new Date().getTime() - 1000 * 60 * 30) ? 'high' : 'low' : 'none', ); }, [node.position]); return ( <> <SidebarItem selected={selected} setSelected={setSelected} actions={ <> <IconButton nested tooltip={PositionConfidence !== 'none' ? 'Fly to Node' : ''} disabled={PositionConfidence === 'none'} onClick={(e): void => { e.stopPropagation(); setSelected(); if (PositionConfidence !== 'none' && node.position) { map?.flyTo({ center: new LngLat( node.position.longitudeI / 1e7, node.position.latitudeI / 1e7, ), zoom: 16, }); } }} icon={ PositionConfidence === 'high' ? ( <MdGpsFixed /> ) : PositionConfidence === 'low' ? ( <MdGpsNotFixed /> ) : ( <MdGpsOff /> ) } /> <IconButton nested tooltip="Show Node Info" onClick={(e): void => { e.stopPropagation(); setInfoOpen(true); }} icon={<FiAlignLeft />} /> </> } > <div className="flex dark:text-white"> <div className="relative m-auto"> {isMyNode && ( <Tooltip content="Your Node"> <m.div whileHover={{ scale: 1.05 }} className="absolute -right-1 -top-1 rounded-full bg-yellow-500 p-0.5" > <BiCrown className="h-3 w-3" /> </m.div> </Tooltip> )} <Hashicon value={node.num.toString()} size={32} /> </div> </div> <div className="my-auto mr-auto text-xs font-semibold dark:text-gray-400"> {node.lastHeard ? new Date(node.lastHeard).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', }) : 'Never'} </div> </SidebarItem> <SidebarOverlay title={`Node ${node.user?.longName ?? 'UNK'} `} open={infoOpen} close={(): void => { setInfoOpen(false); }} direction="x" > <CollapsibleSection title="User" icon={<FiUser />}> <div className="flex p-2"> <div className="m-auto flex flex-col gap-2"> <Hashicon value={node.num.toString()} size={180} /> <div className="text-center text-lg font-medium dark:text-white"> {node?.user?.longName || 'Unknown'} </div> </div> </div> </CollapsibleSection> <CollapsibleSection title="Location" icon={<FiMapPin />}> <> <div className="flex h-10 select-none justify-between rounded-md border border-gray-400 bg-transparent bg-gray-300 px-1 text-gray-500 dark:border-gray-600 dark:bg-secondaryDark dark:text-gray-400 "> {node.position ? ( <> <div className="my-auto px-1"> {(node.position.latitudeI / 1e7).toPrecision(6)} , {(node.position?.longitudeI / 1e7).toPrecision(6)} </div> <CopyButton data={ node.position ? `${node.position.latitudeI / 1e7},${ node.position.longitudeI / 1e7 }` : '' } /> </> ) : ( <div className="my-auto px-1">No location data received</div> )} </div> </> </CollapsibleSection> <CollapsibleSection title="Line of Sight" icon={<IoTelescope />}> <div>Info</div> </CollapsibleSection> <CollapsibleSection title="Administration" icon={<FiSliders />}> <div>Info</div> </CollapsibleSection> <CollapsibleSection title="Debug" icon={<FiCode />}> <> <div className="fixed right-0 mr-6"> <CopyButton data={JSON.stringify(node)} /> </div> <JSONPretty className="max-w-sm" data={node} /> </> </CollapsibleSection> </SidebarOverlay> </> ); };