From 249cecfda22f57c5999cc1e59f275eb3670e6c9a Mon Sep 17 00:00:00 2001 From: Daniel Pupius Date: Wed, 23 Apr 2025 10:38:58 -0700 Subject: [PATCH] Card rendering and maps --- web/.env.development | 3 - web/.env.example | 9 ++ web/.env.production | 3 - web/src/components/Map.tsx | 74 +++++++++ web/src/components/packets/ErrorPacket.tsx | 18 ++- web/src/components/packets/GenericPacket.tsx | 88 +++++++---- web/src/components/packets/KeyValuePair.tsx | 36 +++++ .../components/packets/MapReportPacket.tsx | 145 ++++++++++++++++++ web/src/components/packets/NodeInfoPacket.tsx | 58 +++++-- web/src/components/packets/PacketCard.tsx | 51 ++++-- web/src/components/packets/PacketRenderer.tsx | 8 + web/src/components/packets/PositionPacket.tsx | 86 +++++++---- .../components/packets/TelemetryPacket.tsx | 124 ++++++++++++--- .../components/packets/TextMessagePacket.tsx | 7 +- web/src/components/packets/WaypointPacket.tsx | 110 +++++++++++++ web/src/lib/mapUtils.ts | 56 +++++++ web/src/routes/demo.tsx | 37 +++-- web/src/vite-env.d.ts | 9 ++ 18 files changed, 787 insertions(+), 135 deletions(-) delete mode 100644 web/.env.development create mode 100644 web/.env.example delete mode 100644 web/.env.production create mode 100644 web/src/components/Map.tsx create mode 100644 web/src/components/packets/KeyValuePair.tsx create mode 100644 web/src/components/packets/MapReportPacket.tsx create mode 100644 web/src/components/packets/WaypointPacket.tsx create mode 100644 web/src/lib/mapUtils.ts diff --git a/web/.env.development b/web/.env.development deleted file mode 100644 index c8bd989..0000000 --- a/web/.env.development +++ /dev/null @@ -1,3 +0,0 @@ -# Development environment variables -VITE_API_BASE_URL="http://localhost:8080" -VITE_APP_ENV="development" diff --git a/web/.env.example b/web/.env.example new file mode 100644 index 0000000..0be1c30 --- /dev/null +++ b/web/.env.example @@ -0,0 +1,9 @@ +# Example environment variables for the Meshstream web client +# Copy this file to .env.local and fill in your values + +# Development environment variables +VITE_API_BASE_URL="http://localhost:8080" +VITE_APP_ENV="development" + +# Get one at: https://developers.google.com/maps/documentation/javascript/get-api-key +VITE_GOOGLE_MAPS_API_KEY=OVERRIDE_IN_LOCAL_ENV \ No newline at end of file diff --git a/web/.env.production b/web/.env.production deleted file mode 100644 index 42680b1..0000000 --- a/web/.env.production +++ /dev/null @@ -1,3 +0,0 @@ -# Production environment variables -VITE_API_BASE_URL="" -VITE_APP_ENV="production" diff --git a/web/src/components/Map.tsx b/web/src/components/Map.tsx new file mode 100644 index 0000000..a4521bb --- /dev/null +++ b/web/src/components/Map.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import { getStaticMapUrl, getGoogleMapsUrl } from "../lib/mapUtils"; + +interface MapProps { + latitude: number; + longitude: number; + zoom?: number; + width?: number; + height?: number; + caption?: string; + className?: string; + flush?: boolean; + nightMode?: boolean; +} + +export const Map: React.FC = ({ + latitude, + longitude, + zoom = 14, + width = 300, + height = 200, + caption, + className = "", + flush = false, + nightMode = true +}) => { + const mapUrl = getStaticMapUrl(latitude, longitude, zoom, width, height, nightMode); + const googleMapsUrl = getGoogleMapsUrl(latitude, longitude); + + // Check if Google Maps API key is available + const apiKeyAvailable = Boolean(import.meta.env.VITE_GOOGLE_MAPS_API_KEY); + + const mapContainerClasses = flush + ? `w-full h-full overflow-hidden relative ${className}` + : `${className} relative overflow-hidden rounded-lg border border-neutral-700 bg-neutral-800/50`; + + if (!apiKeyAvailable) { + return ( +
+

+ Map display requires a Google Maps API key. +

+

+ Add VITE_GOOGLE_MAPS_API_KEY to your environment. +

+
+ {latitude.toFixed(6)}, {longitude.toFixed(6)} +
+
+ ); + } + + return ( +
+ + {`Map + {caption && ( +
+ {caption} +
+ )} +
+
+ ); +}; \ No newline at end of file diff --git a/web/src/components/packets/ErrorPacket.tsx b/web/src/components/packets/ErrorPacket.tsx index b84e31c..14737c0 100644 --- a/web/src/components/packets/ErrorPacket.tsx +++ b/web/src/components/packets/ErrorPacket.tsx @@ -17,12 +17,24 @@ export const ErrorPacket: React.FC = ({ packet }) => { return ( } + icon={} iconBgColor="bg-red-500" label="Error" + backgroundColor="bg-red-950/5" > -
- {data.decodeError} +
+
+ {data.decodeError} +
+ + {data.binaryData && ( +
+
Raw Data
+
+ {data.binaryData} +
+
+ )}
); diff --git a/web/src/components/packets/GenericPacket.tsx b/web/src/components/packets/GenericPacket.tsx index f72720b..1242fb6 100644 --- a/web/src/components/packets/GenericPacket.tsx +++ b/web/src/components/packets/GenericPacket.tsx @@ -2,6 +2,7 @@ import React from "react"; import { Packet, PortNum, PortNumByName } from "../../lib/types"; import { Package } from "lucide-react"; import { PacketCard } from "./PacketCard"; +import { KeyValueGrid, KeyValuePair } from "./KeyValuePair"; interface GenericPacketProps { packet: Packet; @@ -25,44 +26,73 @@ export const GenericPacket: React.FC = ({ packet }) => { } // Determine what type of payload is present - if (data.binaryData) return "Binary data"; - if (data.waypoint) return "Waypoint"; - if (data.compressedText) return "Compressed text"; - if (data.mapReport) return "Map report"; - if (data.remoteHardware) return "Remote hardware"; - if (data.routing) return "Routing"; - if (data.admin) return "Admin"; - if (data.audioData) return "Audio"; + if (data.binaryData) return "Binary Data"; + if (data.compressedText) return "Compressed Text"; + if (data.remoteHardware) return "Remote Hardware Control"; + if (data.routing) return "Routing Information"; + if (data.admin) return "Admin Command"; + if (data.audioData) return "Audio Data"; if (data.alert) return `Alert: ${data.alert}`; if (data.reply) return `Reply: ${data.reply}`; - return "Unknown data"; + return "Unknown Data Format"; }; + const portName = getPortName(data.portNum); + return ( } - iconBgColor="bg-neutral-500" - label={getPortName(data.portNum)} + icon={} + iconBgColor="bg-slate-500" + label={portName.replace("_APP", "")} + backgroundColor="bg-slate-950/5" > -
-
-
Port
-
{getPortName(data.portNum)}
-
-
-
Payload
-
{getPayloadDescription()}
-
-
-
To
-
{data.to || "Broadcast"}
-
-
-
Hop Limit
-
{data.hopLimit}
-
+
+ + + + + + + + {data.binaryData && ( +
+
Binary Data
+
+ {data.binaryData} +
+
+ )} + + {data.routing && ( +
+
Routing Information
+
+ {data.routing.errorReason !== undefined && ( +
Error: {PortNum[data.routing.errorReason] || data.routing.errorReason}
+ )} + {data.routing.routeRequest && ( +
Route Request: {data.routing.routeRequest.route?.join(' → ')}
+ )} + {data.routing.routeReply && ( +
Route Reply: {data.routing.routeReply.route?.join(' → ')}
+ )} +
+
+ )}
); diff --git a/web/src/components/packets/KeyValuePair.tsx b/web/src/components/packets/KeyValuePair.tsx new file mode 100644 index 0000000..70b9a15 --- /dev/null +++ b/web/src/components/packets/KeyValuePair.tsx @@ -0,0 +1,36 @@ +import React from "react"; + +interface KeyValuePairProps { + label: string; + value: React.ReactNode; + large?: boolean; +} + +export const KeyValuePair: React.FC = ({ + label, + value, + large = false +}) => { + return ( +
+
+ {label} +
+
+ {value || "—"} +
+
+ ); +}; + +interface KeyValueGridProps { + children: React.ReactNode; +} + +export const KeyValueGrid: React.FC = ({ children }) => { + return ( +
+ {children} +
+ ); +}; \ No newline at end of file diff --git a/web/src/components/packets/MapReportPacket.tsx b/web/src/components/packets/MapReportPacket.tsx new file mode 100644 index 0000000..879ebd5 --- /dev/null +++ b/web/src/components/packets/MapReportPacket.tsx @@ -0,0 +1,145 @@ +import React from "react"; +import { Packet } from "../../lib/types"; +import { Map as MapIcon } from "lucide-react"; +import { PacketCard } from "./PacketCard"; +import { KeyValueGrid, KeyValuePair } from "./KeyValuePair"; +import { Map } from "../Map"; + +interface MapReportPacketProps { + packet: Packet; +} + +export const MapReportPacket: React.FC = ({ packet }) => { + const { data } = packet; + const mapReport = data.mapReport; + + if (!mapReport || !mapReport.nodes || mapReport.nodes.length === 0) { + return null; + } + + // Get the center point for the map (average of all node positions) + const getMapCenter = () => { + let validPositions = 0; + let sumLat = 0; + let sumLng = 0; + + mapReport.nodes.forEach(node => { + if (node.position && node.position.latitudeI && node.position.longitudeI) { + sumLat += node.position.latitudeI * 1e-7; + sumLng += node.position.longitudeI * 1e-7; + validPositions++; + } + }); + + if (validPositions > 0) { + return { + latitude: sumLat / validPositions, + longitude: sumLng / validPositions, + }; + } + + return null; + }; + + const center = getMapCenter(); + + const formatTimestamp = (timestamp: number) => { + return new Date(timestamp * 1000).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit' + }); + }; + + return ( + } + iconBgColor="bg-cyan-500" + label="Map Report" + backgroundColor="bg-cyan-950/5" + > +
+
+
+

+ Network Map ({mapReport.nodes.length} Nodes) +

+ +
+ {mapReport.nodes.map((node, index) => ( +
+
+
+ {node.user?.longName || `Node ${node.num?.toString(16)}`} +
+ {node.lastHeard && ( +
+ {formatTimestamp(node.lastHeard)} +
+ )} +
+ + + {node.user?.shortName && ( + + )} + {node.num !== undefined && ( + + )} + {node.user?.hwModel && ( + + )} + {node.snr !== undefined && ( + + )} + {node.position?.latitudeI && node.position?.longitudeI && ( + <> + + + + )} + +
+ ))} +
+
+ + {center && ( +
+ +
+ )} +
+
+
+ ); +}; \ No newline at end of file diff --git a/web/src/components/packets/NodeInfoPacket.tsx b/web/src/components/packets/NodeInfoPacket.tsx index 6baaffa..ac7716c 100644 --- a/web/src/components/packets/NodeInfoPacket.tsx +++ b/web/src/components/packets/NodeInfoPacket.tsx @@ -2,6 +2,7 @@ import React from "react"; import { Packet } from "../../lib/types"; import { User } from "lucide-react"; import { PacketCard } from "./PacketCard"; +import { KeyValueGrid, KeyValuePair } from "./KeyValuePair"; interface NodeInfoPacketProps { packet: Packet; @@ -18,23 +19,52 @@ export const NodeInfoPacket: React.FC = ({ packet }) => { return ( } + icon={} iconBgColor="bg-purple-500" label="Node Info" + backgroundColor="bg-purple-950/5" > -
-
-
Long Name
-
{nodeInfo.longName || "—"}
-
-
-
Short Name
-
{nodeInfo.shortName || "—"}
-
-
-
ID
-
{nodeInfo.id || "—"}
-
+
+ + + + + {nodeInfo.id || "—"}} + /> + {nodeInfo.hwModel && ( + + )} + {nodeInfo.role && ( + + )} + {nodeInfo.batteryLevel !== undefined && ( + + )} + {nodeInfo.lastHeard && ( + + )} +
); diff --git a/web/src/components/packets/PacketCard.tsx b/web/src/components/packets/PacketCard.tsx index b25ca0f..646366d 100644 --- a/web/src/components/packets/PacketCard.tsx +++ b/web/src/components/packets/PacketCard.tsx @@ -7,6 +7,7 @@ interface PacketCardProps { iconBgColor: string; label: string; children: ReactNode; + backgroundColor?: string; } export const PacketCard: React.FC = ({ @@ -15,30 +16,46 @@ export const PacketCard: React.FC = ({ iconBgColor, label, children, + backgroundColor = "bg-neutral-500/5", }) => { const { data } = packet; return ( -
-
-
-
{icon}
- - From:{" "} - {data.from ? `!${data.from.toString(16).toLowerCase()}` : "Unknown"} - +
+ {/* Card Header with all metadata */} +
+
+ {/* Left side: Icon, From, Channel */} +
+
+ {React.cloneElement(icon as React.ReactElement, { + className: "h-3.5 w-3.5 text-white" + })} +
+ + {data.from ? `!${data.from.toString(16).toLowerCase()}` : "Unknown"} + + + Channel: {packet.info.channel} + +
+ + {/* Right side: ID and Type */} +
+ + ID: {data.id || "None"} + + + {label} + +
- - ID: {data.id || "No ID"} -
-
{children}
- -
- Channel: {packet.info.channel} - {label} + {/* Card Content */} +
+ {children}
); -}; +}; \ No newline at end of file diff --git a/web/src/components/packets/PacketRenderer.tsx b/web/src/components/packets/PacketRenderer.tsx index 59e5c26..c4b17ce 100644 --- a/web/src/components/packets/PacketRenderer.tsx +++ b/web/src/components/packets/PacketRenderer.tsx @@ -5,6 +5,8 @@ import { PositionPacket } from "./PositionPacket"; import { NodeInfoPacket } from "./NodeInfoPacket"; import { TelemetryPacket } from "./TelemetryPacket"; import { ErrorPacket } from "./ErrorPacket"; +import { WaypointPacket } from "./WaypointPacket"; +import { MapReportPacket } from "./MapReportPacket"; import { GenericPacket } from "./GenericPacket"; interface PacketRendererProps { @@ -37,6 +39,12 @@ export const PacketRenderer: React.FC = ({ packet }) => { case PortNum.TELEMETRY_APP: return ; + + case PortNum.WAYPOINT_APP: + return ; + + case PortNum.MAP_REPORT_APP: + return ; default: return ; diff --git a/web/src/components/packets/PositionPacket.tsx b/web/src/components/packets/PositionPacket.tsx index b03f72a..be9be07 100644 --- a/web/src/components/packets/PositionPacket.tsx +++ b/web/src/components/packets/PositionPacket.tsx @@ -2,6 +2,8 @@ import React from "react"; import { Packet } from "../../lib/types"; import { MapPin } from "lucide-react"; import { PacketCard } from "./PacketCard"; +import { KeyValueGrid, KeyValuePair } from "./KeyValuePair"; +import { Map } from "../Map"; interface PositionPacketProps { packet: Packet; @@ -19,47 +21,69 @@ export const PositionPacket: React.FC = ({ packet }) => { const latitude = position.latitudeI ? position.latitudeI * 1e-7 : undefined; const longitude = position.longitudeI ? position.longitudeI * 1e-7 : undefined; + // Format time without seconds + const formattedTime = position.time + ? new Date(position.time * 1000).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}) + : 'N/A'; + return ( } + icon={} iconBgColor="bg-emerald-500" label="Position" + backgroundColor="bg-emerald-950/5" > -
+
-
Latitude
-
{latitude !== undefined ? latitude.toFixed(6) : 'N/A'}
+ + + + {position.altitude && ( + + )} + {position.time && ( + + )} + {position.locationSource && ( + + )} + {position.satsInView && ( + + )} +
-
-
Longitude
-
{longitude !== undefined ? longitude.toFixed(6) : 'N/A'}
-
- {position.altitude && ( -
-
Altitude
-
{position.altitude.toFixed(1)}m
-
- )} - {position.time && ( -
-
Time
-
{new Date(position.time * 1000).toLocaleTimeString()}
-
- )} - {position.locationSource && ( -
-
Source
-
{position.locationSource.replace('LOC_', '')}
-
- )} - {position.satsInView && ( -
-
Satellites
-
{position.satsInView}
+ + {latitude !== undefined && longitude !== undefined && ( +
+
)}
); -}; +}; \ No newline at end of file diff --git a/web/src/components/packets/TelemetryPacket.tsx b/web/src/components/packets/TelemetryPacket.tsx index 00988e2..2e43c6b 100644 --- a/web/src/components/packets/TelemetryPacket.tsx +++ b/web/src/components/packets/TelemetryPacket.tsx @@ -2,6 +2,7 @@ import React from "react"; import { Packet } from "../../lib/types"; import { BarChart } from "lucide-react"; import { PacketCard } from "./PacketCard"; +import { KeyValueGrid, KeyValuePair } from "./KeyValuePair"; interface TelemetryPacketProps { packet: Packet; @@ -15,41 +16,118 @@ export const TelemetryPacket: React.FC = ({ packet }) => { return null; } - // Helper function to display telemetry fields - const renderTelemetryFields = () => { - const entries = Object.entries(telemetry).filter(([key]) => key !== 'time'); - - if (entries.length === 0) { - return
No telemetry data available
; - } + // Helper function to render device metrics + const renderDeviceMetrics = () => { + if (!telemetry.deviceMetrics) return null; + const metrics = telemetry.deviceMetrics; return ( -
- {entries.map(([key, value]) => ( -
-
{key.charAt(0).toUpperCase() + key.slice(1)}
-
{typeof value === 'number' ? value.toFixed(2) : String(value)}
-
- ))} - - {telemetry.time && ( -
-
Time
-
{new Date(telemetry.time * 1000).toLocaleString()}
-
- )} +
+

Device

+ + {metrics.batteryLevel !== undefined && ( + + )} + {metrics.voltage !== undefined && ( + + )} + {metrics.channelUtilization !== undefined && ( + + )} + {metrics.uptimeSeconds !== undefined && ( + + )} +
); }; + // Helper function to render environment metrics + const renderEnvironmentMetrics = () => { + if (!telemetry.environmentMetrics) return null; + + const metrics = telemetry.environmentMetrics; + return ( +
+

Environment

+ + {metrics.temperature !== undefined && ( + + )} + {metrics.relativeHumidity !== undefined && ( + + )} + {metrics.barometricPressure !== undefined && ( + + )} + {metrics.lux !== undefined && ( + + )} + +
+ ); + }; + + // Format uptime in a readable way + const formatUptime = (seconds: number): string => { + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + const mins = Math.floor((seconds % 3600) / 60); + + if (days > 0) { + return `${days}d ${hours}h`; + } + if (hours > 0) { + return `${hours}h ${mins}m`; + } + return `${mins}m`; + }; + return ( } + icon={} iconBgColor="bg-amber-500" label="Telemetry" + backgroundColor="bg-amber-950/5" > - {renderTelemetryFields()} +
+ {telemetry.time && ( +
+ +
+ )} + + {renderDeviceMetrics()} + {renderEnvironmentMetrics()} +
); }; \ No newline at end of file diff --git a/web/src/components/packets/TextMessagePacket.tsx b/web/src/components/packets/TextMessagePacket.tsx index 646c107..8efeeec 100644 --- a/web/src/components/packets/TextMessagePacket.tsx +++ b/web/src/components/packets/TextMessagePacket.tsx @@ -13,11 +13,14 @@ export const TextMessagePacket: React.FC = ({ packet }) return ( } + icon={} iconBgColor="bg-blue-500" label="Text Message" + backgroundColor="bg-blue-950/5" > - {data.textMessage || "Empty message"} +
+ {data.textMessage || "Empty message"} +
); }; \ No newline at end of file diff --git a/web/src/components/packets/WaypointPacket.tsx b/web/src/components/packets/WaypointPacket.tsx new file mode 100644 index 0000000..3bcd1e6 --- /dev/null +++ b/web/src/components/packets/WaypointPacket.tsx @@ -0,0 +1,110 @@ +import React from "react"; +import { Packet } from "../../lib/types"; +import { MapPin } from "lucide-react"; +import { PacketCard } from "./PacketCard"; +import { KeyValueGrid, KeyValuePair } from "./KeyValuePair"; +import { Map } from "../Map"; + +interface WaypointPacketProps { + packet: Packet; +} + +export const WaypointPacket: React.FC = ({ packet }) => { + const { data } = packet; + const waypoint = data.waypoint; + + if (!waypoint) { + return null; + } + + // Convert coordinates + const latitude = waypoint.latitudeI ? waypoint.latitudeI * 1e-7 : undefined; + const longitude = waypoint.longitudeI ? waypoint.longitudeI * 1e-7 : undefined; + + // Format expire time if available + const expireTime = waypoint.expire && waypoint.expire > 0 + ? new Date(waypoint.expire * 1000).toLocaleString([], { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) + : undefined; + + return ( + } + iconBgColor="bg-violet-500" + label="Waypoint" + backgroundColor="bg-violet-950/5" + > +
+ {waypoint.name && ( + + )} + +
+
+ + {waypoint.id !== undefined && ( + + )} + {latitude !== undefined && ( + + )} + {longitude !== undefined && ( + + )} + {expireTime && ( + + )} + {waypoint.lockedTo !== undefined && waypoint.lockedTo > 0 && ( + + )} + + + {waypoint.description && ( +
+
Description
+
{waypoint.description}
+
+ )} +
+ + {latitude !== undefined && longitude !== undefined && ( +
+ +
+ )} +
+
+
+ ); +}; \ No newline at end of file diff --git a/web/src/lib/mapUtils.ts b/web/src/lib/mapUtils.ts new file mode 100644 index 0000000..8fe7c2e --- /dev/null +++ b/web/src/lib/mapUtils.ts @@ -0,0 +1,56 @@ +/** + * Utility functions for working with maps and coordinates + */ + +/** + * Generate a Google Maps Static API URL for a given latitude and longitude + * @param latitude The latitude in decimal degrees + * @param longitude The longitude in decimal degrees + * @param zoom The zoom level (1-20) + * @param width The image width in pixels + * @param height The image height in pixels + * @param nightMode Whether to use dark styling for the map + * @returns A URL string for the Google Maps Static API + */ +export const getStaticMapUrl = ( + latitude: number, + longitude: number, + zoom: number = 15, + width: number = 300, + height: number = 200, + nightMode: boolean = true +): string => { + // Get API key from environment variable + const apiKey = import.meta.env.VITE_GOOGLE_MAPS_API_KEY || ""; + + // Build the URL + const mapUrl = new URL("https://maps.googleapis.com/maps/api/staticmap"); + + // Add parameters + mapUrl.searchParams.append("center", `${latitude},${longitude}`); + mapUrl.searchParams.append("zoom", zoom.toString()); + mapUrl.searchParams.append("size", `${width}x${height}`); + mapUrl.searchParams.append("key", apiKey); + mapUrl.searchParams.append("format", "png"); + mapUrl.searchParams.append("scale", "2"); // Retina display support + + // Add marker + mapUrl.searchParams.append("markers", `color:red|${latitude},${longitude}`); + + // Apply night mode styling using the simpler approach + if (nightMode) { + mapUrl.searchParams.append("style", "invert_lightness:true"); + } + + return mapUrl.toString(); +}; + +/** + * Create a Google Maps URL to open the location in Google Maps + */ +export const getGoogleMapsUrl = ( + latitude: number, + longitude: number +): string => { + return `https://www.google.com/maps?q=${latitude},${longitude}`; +}; diff --git a/web/src/routes/demo.tsx b/web/src/routes/demo.tsx index c65b492..a418dd5 100644 --- a/web/src/routes/demo.tsx +++ b/web/src/routes/demo.tsx @@ -1,9 +1,13 @@ +import React from "react"; +import { createFileRoute } from "@tanstack/react-router"; import { PageWrapper } from "../components/PageWrapper"; import { TextMessagePacket } from "../components/packets/TextMessagePacket"; import { PositionPacket } from "../components/packets/PositionPacket"; import { NodeInfoPacket } from "../components/packets/NodeInfoPacket"; import { TelemetryPacket } from "../components/packets/TelemetryPacket"; import { ErrorPacket } from "../components/packets/ErrorPacket"; +import { WaypointPacket } from "../components/packets/WaypointPacket"; +import { MapReportPacket } from "../components/packets/MapReportPacket"; import { GenericPacket } from "../components/packets/GenericPacket"; // Import sample data @@ -12,6 +16,12 @@ import positionData from "../../fixtures/position.json"; import nodeInfoData from "../../fixtures/nodeinfo.json"; import telemetryData from "../../fixtures/telemetry.json"; import decodeErrorData from "../../fixtures/decode_error.json"; +import waypointData from "../../fixtures/waypoint.json"; +import mapReportData from "../../fixtures/map_report.json"; + +export const Route = createFileRoute("/demo")({ + component: DemoPage, +}); export function DemoPage() { return ( @@ -54,6 +64,20 @@ export function DemoPage() { + +
+

+ Waypoint Packet +

+ +
+ +
+

+ Map Report Packet +

+ +

@@ -64,23 +88,16 @@ export function DemoPage() {

- Generic Packet + Generic Packet (Unknown Type)

diff --git a/web/src/vite-env.d.ts b/web/src/vite-env.d.ts index 11f02fe..b7e73e9 100644 --- a/web/src/vite-env.d.ts +++ b/web/src/vite-env.d.ts @@ -1 +1,10 @@ /// + +interface ImportMetaEnv { + readonly VITE_GOOGLE_MAPS_API_KEY: string; + // Add other environment variables as needed +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} \ No newline at end of file