Working on UI

This commit is contained in:
Daniel Pupius
2025-04-22 16:42:22 -07:00
parent 60c8878436
commit 095ff560b0
20 changed files with 389 additions and 175 deletions

View File

@@ -1,13 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Meshstream - A web interface for viewing Meshtastic network traffic" />
<title>Meshstream</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="ERSN Mesh :: Meshtastic activity in the Ebbett's Pass region of Highway 4, CA." />
<title>ERSN Mesh</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -29,12 +29,14 @@
"@tanstack/router-devtools": "^1.116.0",
"@tanstack/router-vite-plugin": "^1.116.1",
"leaflet": "^1.9.4",
"lucide-react": "^0.503.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-leaflet": "^5.0.0",
"react-redux": "^9.2.0"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.4",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
@@ -51,6 +53,7 @@
"eslint-plugin-react-refresh": "^0.4.20",
"jsdom": "^26.1.0",
"postcss": "^8.5.3",
"tailwindcss": "^4.1.4",
"typescript": "^5.8.3",
"typescript-eslint": "^8.31.0",
"vite": "^6.3.2",

59
web/pnpm-lock.yaml generated
View File

@@ -29,6 +29,9 @@ dependencies:
leaflet:
specifier: ^1.9.4
version: 1.9.4
lucide-react:
specifier: ^0.503.0
version: 0.503.0(react@19.1.0)
react:
specifier: ^19.1.0
version: 19.1.0
@@ -43,6 +46,9 @@ dependencies:
version: 9.2.0(@types/react@19.1.2)(react@19.1.0)(redux@5.0.1)
devDependencies:
'@tailwindcss/vite':
specifier: ^4.1.4
version: 4.1.4(vite@6.3.2)
'@testing-library/jest-dom':
specifier: ^6.6.3
version: 6.6.3
@@ -91,6 +97,9 @@ devDependencies:
postcss:
specifier: ^8.5.3
version: 8.5.3
tailwindcss:
specifier: ^4.1.4
version: 4.1.4
typescript:
specifier: ^5.8.3
version: 5.8.3
@@ -892,7 +901,6 @@ packages:
jiti: 2.4.2
lightningcss: 1.29.2
tailwindcss: 4.1.4
dev: false
/@tailwindcss/oxide-android-arm64@4.1.4:
resolution: {integrity: sha512-xMMAe/SaCN/vHfQYui3fqaBDEXMu22BVwQ33veLc8ep+DNy7CWN52L+TTG9y1K397w9nkzv+Mw+mZWISiqhmlA==}
@@ -900,7 +908,6 @@ packages:
cpu: [arm64]
os: [android]
requiresBuild: true
dev: false
optional: true
/@tailwindcss/oxide-darwin-arm64@4.1.4:
@@ -909,7 +916,6 @@ packages:
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/@tailwindcss/oxide-darwin-x64@4.1.4:
@@ -918,7 +924,6 @@ packages:
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/@tailwindcss/oxide-freebsd-x64@4.1.4:
@@ -927,7 +932,6 @@ packages:
cpu: [x64]
os: [freebsd]
requiresBuild: true
dev: false
optional: true
/@tailwindcss/oxide-linux-arm-gnueabihf@4.1.4:
@@ -936,7 +940,6 @@ packages:
cpu: [arm]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@tailwindcss/oxide-linux-arm64-gnu@4.1.4:
@@ -945,7 +948,6 @@ packages:
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@tailwindcss/oxide-linux-arm64-musl@4.1.4:
@@ -954,7 +956,6 @@ packages:
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@tailwindcss/oxide-linux-x64-gnu@4.1.4:
@@ -963,7 +964,6 @@ packages:
cpu: [x64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@tailwindcss/oxide-linux-x64-musl@4.1.4:
@@ -972,7 +972,6 @@ packages:
cpu: [x64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@tailwindcss/oxide-wasm32-wasi@4.1.4:
@@ -980,7 +979,6 @@ packages:
engines: {node: '>=14.0.0'}
cpu: [wasm32]
requiresBuild: true
dev: false
optional: true
bundledDependencies:
- '@napi-rs/wasm-runtime'
@@ -996,7 +994,6 @@ packages:
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@tailwindcss/oxide-win32-x64-msvc@4.1.4:
@@ -1005,7 +1002,6 @@ packages:
cpu: [x64]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@tailwindcss/oxide@4.1.4:
@@ -1024,7 +1020,6 @@ packages:
'@tailwindcss/oxide-wasm32-wasi': 4.1.4
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.4
'@tailwindcss/oxide-win32-x64-msvc': 4.1.4
dev: false
/@tailwindcss/postcss@4.1.4:
resolution: {integrity: sha512-bjV6sqycCEa+AQSt2Kr7wpGF1bOZJ5wsqnLEkqSbM/JEHxx/yhMH8wHmdkPyApF9xhHeMSwnnkDUUMMM/hYnXw==}
@@ -1036,6 +1031,17 @@ packages:
tailwindcss: 4.1.4
dev: false
/@tailwindcss/vite@4.1.4(vite@6.3.2):
resolution: {integrity: sha512-4UQeMrONbvrsXKXXp/uxmdEN5JIJ9RkH7YVzs6AMxC/KC1+Np7WZBaNIco7TEjlkthqxZbt8pU/ipD+hKjm80A==}
peerDependencies:
vite: ^5.2.0 || ^6
dependencies:
'@tailwindcss/node': 4.1.4
'@tailwindcss/oxide': 4.1.4
tailwindcss: 4.1.4
vite: 6.3.2
dev: true
/@tanstack/history@1.115.0:
resolution: {integrity: sha512-K7JJNrRVvyjAVnbXOH2XLRhFXDkeP54Kt2P4FR1Kl2KDGlIbkua5VqZQD2rot3qaDrpufyUa63nuLai1kOLTsQ==}
engines: {node: '>=12'}
@@ -2026,7 +2032,6 @@ packages:
/detect-libc@2.0.4:
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
engines: {node: '>=8'}
dev: false
/diff@7.0.0:
resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==}
@@ -2066,7 +2071,6 @@ packages:
dependencies:
graceful-fs: 4.2.11
tapable: 2.2.1
dev: false
/entities@6.0.0:
resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==}
@@ -2593,7 +2597,6 @@ packages:
/graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
dev: false
/graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
@@ -2919,7 +2922,6 @@ packages:
/jiti@2.4.2:
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
hasBin: true
dev: false
/js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -3022,7 +3024,6 @@ packages:
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/lightningcss-darwin-x64@1.29.2:
@@ -3031,7 +3032,6 @@ packages:
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/lightningcss-freebsd-x64@1.29.2:
@@ -3040,7 +3040,6 @@ packages:
cpu: [x64]
os: [freebsd]
requiresBuild: true
dev: false
optional: true
/lightningcss-linux-arm-gnueabihf@1.29.2:
@@ -3049,7 +3048,6 @@ packages:
cpu: [arm]
os: [linux]
requiresBuild: true
dev: false
optional: true
/lightningcss-linux-arm64-gnu@1.29.2:
@@ -3058,7 +3056,6 @@ packages:
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/lightningcss-linux-arm64-musl@1.29.2:
@@ -3067,7 +3064,6 @@ packages:
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/lightningcss-linux-x64-gnu@1.29.2:
@@ -3076,7 +3072,6 @@ packages:
cpu: [x64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/lightningcss-linux-x64-musl@1.29.2:
@@ -3085,7 +3080,6 @@ packages:
cpu: [x64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/lightningcss-win32-arm64-msvc@1.29.2:
@@ -3094,7 +3088,6 @@ packages:
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: false
optional: true
/lightningcss-win32-x64-msvc@1.29.2:
@@ -3103,7 +3096,6 @@ packages:
cpu: [x64]
os: [win32]
requiresBuild: true
dev: false
optional: true
/lightningcss@1.29.2:
@@ -3122,7 +3114,6 @@ packages:
lightningcss-linux-x64-musl: 1.29.2
lightningcss-win32-arm64-msvc: 1.29.2
lightningcss-win32-x64-msvc: 1.29.2
dev: false
/locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
@@ -3159,6 +3150,14 @@ packages:
dependencies:
yallist: 3.1.1
/lucide-react@0.503.0(react@19.1.0):
resolution: {integrity: sha512-HGGkdlPWQ0vTF8jJ5TdIqhQXZi6uh3LnNgfZ8MHiuxFfX3RZeA79r2MW2tHAZKlAVfoNE8esm3p+O6VkIvpj6w==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
dependencies:
react: 19.1.0
dev: false
/lz-string@1.5.0:
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
hasBin: true
@@ -3869,12 +3868,10 @@ packages:
/tailwindcss@4.1.4:
resolution: {integrity: sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==}
dev: false
/tapable@2.2.1:
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
engines: {node: '>=6'}
dev: false
/tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}

View File

@@ -3,4 +3,4 @@ export default {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
};
};

View File

@@ -0,0 +1,55 @@
import React from "react";
import { WifiOff, Wifi, WifiLow, Activity, AlertCircle } from "lucide-react";
interface ConnectionStatusProps {
status: string;
}
export const ConnectionStatus: React.FC<ConnectionStatusProps> = ({
status,
}) => {
// Determine connection status type
const getStatusInfo = () => {
if (status.includes("error") || status.includes("Error")) {
return {
icon: <AlertCircle className="h-5 w-5 text-red-400" />,
text: "Connection Error",
colorClass: "text-red-400",
};
} else if (status.includes("Reconnecting")) {
return {
icon: <WifiLow className="h-5 w-5 text-amber-400" />,
text: "Reconnecting",
colorClass: "text-amber-400",
};
} else if (status.includes("Connecting")) {
return {
icon: <Activity className="h-5 w-5 text-blue-400" />,
text: "Connecting",
colorClass: "text-blue-400",
};
} else if (status.includes("Connected")) {
return {
icon: <Wifi className="h-5 w-5 text-green-400" />,
text: "Connected",
colorClass: "text-green-400",
};
} else {
// Default state or unknown status
return {
icon: <WifiOff className="h-5 w-5 text-neutral-400" />,
text: status || "Unknown",
colorClass: "text-neutral-400",
};
}
};
const { icon, text, colorClass } = getStatusInfo();
return (
<div className="flex items-center space-x-2 bg-neutral-800 p-4 border-1 border-b-neutral-700 border-r-neutral-700 border-t-neutral-950 border-l-neutral-950 rounded-sm">
{icon}
<span className={`text-sm font-medium ${colorClass}`}>{text}</span>
</div>
);
};

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React from "react";
interface FilterProps {
onChange: (filter: string) => void;
@@ -8,13 +8,16 @@ interface FilterProps {
export const Filter: React.FC<FilterProps> = ({ onChange, value }) => {
return (
<div className="mb-4">
<label htmlFor="filter" className="block text-sm font-medium text-gray-700 mb-1">
<label
htmlFor="filter"
className="block text-sm font-medium text-neutral-700 mb-1"
>
Filter Packets
</label>
<input
type="text"
id="filter"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
className="w-full px-3 py-2 border border-neutral-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
placeholder="Filter by type, sender, etc."
value={value}
onChange={(e) => onChange(e.target.value)}

View File

@@ -1,41 +1,57 @@
import React from 'react';
import React from "react";
import { Info, AlertTriangle, AlertCircle } from "lucide-react";
interface InfoMessageProps {
message: string;
type?: 'info' | 'warning' | 'error';
type?: "info" | "warning" | "error";
}
export const InfoMessage: React.FC<InfoMessageProps> = ({
message,
type = 'info'
export const InfoMessage: React.FC<InfoMessageProps> = ({
message,
type = "info",
}) => {
const getBgColor = () => {
switch (type) {
case 'warning':
return 'bg-yellow-50 border-yellow-200';
case 'error':
return 'bg-red-50 border-red-200';
case 'info':
case "warning":
return "bg-neutral-700 border-neutral-600";
case "error":
return "bg-neutral-800 border-neutral-700";
case "info":
default:
return 'bg-blue-50 border-blue-200';
return "bg-neutral-800 border-neutral-700";
}
};
const getTextColor = () => {
switch (type) {
case 'warning':
return 'text-yellow-800';
case 'error':
return 'text-red-800';
case 'info':
case "warning":
return "text-amber-400";
case "error":
return "text-red-400";
case "info":
default:
return 'text-blue-800';
return "text-neutral-300";
}
};
const getIcon = () => {
switch (type) {
case "warning":
return <AlertTriangle className="h-5 w-5 text-amber-400" />;
case "error":
return <AlertCircle className="h-5 w-5 text-red-400" />;
case "info":
default:
return <Info className="h-5 w-5 text-neutral-400" />;
}
};
return (
<div className={`p-3 mb-4 rounded border ${getBgColor()}`}>
<p className={`text-sm ${getTextColor()}`}>{message}</p>
<div className={`p-3 mb-4 rounded border ${getBgColor()} shadow-inner`}>
<div className="flex items-start">
<div className="flex-shrink-0 mt-0.5 mr-3">{getIcon()}</div>
<p className={`text-sm ${getTextColor()}`}>{message}</p>
</div>
</div>
);
};
};

View File

@@ -1,24 +1,44 @@
import React from 'react';
import React from "react";
import { Packet } from "../lib/types";
interface MessageDisplayProps {
message: any; // Will be properly typed once we have the protobuf structures
message: Packet;
}
export const MessageDisplay: React.FC<MessageDisplayProps> = ({ message }) => {
// The data structure will be refined as we integrate with the protobuf definitions
const { data } = message;
const getMessageContent = () => {
if (data.text_message) {
return data.text_message;
} else if (data.position) {
return `Position: ${data.position.latitude}, ${data.position.longitude}`;
} else if (data.node_info) {
return `Node Info: ${data.node_info.longName || data.node_info.shortName}`;
} else if (data.telemetry) {
return "Telemetry data";
} else if (data.decode_error) {
return `Error: ${data.decode_error}`;
}
return "Unknown message type";
};
return (
<div className="p-4 border rounded shadow-sm bg-white">
<div className="p-4 border border-neutral-700 rounded bg-neutral-800 shadow-inner">
<div className="flex justify-between mb-2">
<span className="font-medium">{message.from || 'Unknown'}</span>
<span className="text-gray-500 text-sm">
{message.timestamp ? new Date(message.timestamp).toLocaleString() : 'No timestamp'}
<span className="font-medium text-neutral-200">
From: {data.from || "Unknown"}
</span>
<span className="text-neutral-400 text-sm">
ID: {data.id || "No ID"}
</span>
</div>
<div className="mb-2">
{message.text || 'No content'}
</div>
<div className="text-xs text-gray-500">
ID: {message.id || 'No ID'}
<div className="mb-2 text-neutral-300">{getMessageContent()}</div>
<div className="mt-3 flex justify-between items-center">
<span className="text-xs text-neutral-500">
Channel: {message.info.channel}
</span>
<span className="text-xs text-neutral-500">Type: {data.port_num}</span>
</div>
</div>
);

View File

@@ -0,0 +1,57 @@
import React from "react";
import { Link } from "@tanstack/react-router";
import { ConnectionStatus } from "./ConnectionStatus";
import { Separator } from "./Separator";
import { SITE_TITLE } from "../lib/config";
interface NavProps {
connectionStatus: string;
}
export const Nav: React.FC<NavProps> = ({ connectionStatus }) => {
return (
<aside className="w-64 text-neutral-100 h-screen fixed left-0 top-0 flex flex-col">
{/* Logo section */}
<div className="p-4 mb-2">
<h1 className="text-xl font-bold">{SITE_TITLE}</h1>
</div>
<Separator />
<nav className="flex-1 pt-4">
<ul className="space-y-2">
<li>
<Link
to="/"
className="block px-4 py-2 hover:bg-neutral-800 transition-colors"
activeProps={{
className:
"block px-4 py-2 bg-neutral-800 border-l-4 border-neutral-500",
}}
>
Home
</Link>
</li>
<li>
<Link
to="/packets"
className="block px-4 py-2 hover:bg-neutral-800 transition-colors"
activeProps={{
className:
"block px-4 py-2 bg-neutral-800 border-l-4 border-neutral-500",
}}
>
Packets
</Link>
</li>
</ul>
</nav>
<Separator />
<div className="px-4 py-2 pb-6">
<ConnectionStatus status={connectionStatus} />
</div>
</aside>
);
};

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React from "react";
interface PacketDetailsProps {
packet: any; // Will be properly typed once we have the protobuf structures
@@ -6,9 +6,9 @@ interface PacketDetailsProps {
export const PacketDetails: React.FC<PacketDetailsProps> = ({ packet }) => {
return (
<div className="p-4 border rounded bg-gray-50">
<div className="p-4 border rounded bg-neutral-50">
<h3 className="text-lg font-semibold mb-2">Packet Details</h3>
<pre className="text-xs overflow-auto p-2 bg-gray-100 rounded">
<pre className="text-xs overflow-auto p-2 bg-neutral-100 rounded">
{JSON.stringify(packet, null, 2)}
</pre>
</div>

View File

@@ -1,27 +1,32 @@
import React from 'react';
import { useAppSelector } from '../hooks';
import React from "react";
import { useAppSelector } from "../hooks";
export const PacketList: React.FC = () => {
const { packets, loading, error } = useAppSelector(state => state.packets);
const { packets, loading, error } = useAppSelector((state) => state.packets);
if (loading) {
return <div className="p-4">Loading...</div>;
return <div className="p-4 text-neutral-300">Loading...</div>;
}
if (error) {
return <div className="p-4 text-red-500">Error: {error}</div>;
return <div className="p-4 text-red-400">Error: {error}</div>;
}
if (packets.length === 0) {
return <div className="p-4">No packets received yet</div>;
return <div className="p-4 text-neutral-400">No packets received yet</div>;
}
return (
<div className="p-4">
<h2 className="text-xl font-bold mb-4">Received Packets</h2>
<h2 className="text-xl font-bold mb-4 text-neutral-200">
Received Packets
</h2>
<ul className="space-y-2">
{packets.map(packet => (
<li key={packet.id} className="p-2 border rounded">
{packets.map((packet) => (
<li
key={packet.id}
className="p-3 border border-neutral-700 rounded bg-neutral-900 shadow-inner text-neutral-300 hover:bg-neutral-800 transition-colors"
>
{packet.id}
</li>
))}

View File

@@ -0,0 +1,13 @@
import React from "react";
interface SeparatorProps {
className?: string;
}
export const Separator: React.FC<SeparatorProps> = ({ className = "" }) => {
return (
<div
className={`mx-4 h-px border-t-1 border-t-neutral-900 border-b-1 border-b-neutral-700/80 my-3 ${className}`}
></div>
);
};

View File

@@ -3,3 +3,6 @@ export * from './MessageDisplay';
export * from './PacketDetails';
export * from './Filter';
export * from './InfoMessage';
export * from './ConnectionStatus';
export * from './Nav';
export * from './Separator';

View File

@@ -7,6 +7,9 @@ export const IS_DEV = import.meta.env.DEV;
export const IS_PROD = import.meta.env.PROD;
export const APP_ENV = import.meta.env.VITE_APP_ENV || 'development';
// Site configuration
export const SITE_TITLE = import.meta.env.VITE_SITE_TITLE || 'ERSN Mesh';
// API URL configuration
const getApiBaseUrl = (): string => {
// In production, use the same domain (empty string base URL)
@@ -24,4 +27,4 @@ export const API_BASE_URL = getApiBaseUrl();
export const API_ENDPOINTS = {
STREAM: `${API_BASE_URL}/api/stream`,
RECENT_PACKETS: `${API_BASE_URL}/api/packets/recent`,
};
};

View File

@@ -1,36 +1,39 @@
import { Link, Outlet } from '@tanstack/react-router';
import { Outlet } from "@tanstack/react-router";
import { useState, useEffect } from "react";
import { Nav } from "../components";
import { streamPackets, StreamEvent } from "../lib/api";
export default function Root() {
const [connectionStatus, setConnectionStatus] =
useState<string>("Connecting...");
useEffect(() => {
// Set up Server-Sent Events connection
const cleanup = streamPackets(
// Event handler for all event types
(event: StreamEvent) => {
if (event.type === "info") {
// Handle info events (connection status, etc.)
setConnectionStatus(event.data);
}
},
// On error
() => {
setConnectionStatus("Connection error. Reconnecting...");
}
);
// Clean up connection when component unmounts
return cleanup;
}, []);
return (
<div className="min-h-screen bg-gray-100">
<header className="bg-blue-600 text-white shadow-md">
<div className="container mx-auto px-4 py-3">
<div className="flex justify-between items-center">
<h1 className="text-2xl font-bold">Meshstream</h1>
<nav className="space-x-4">
<Link
to="/"
className="hover:underline"
activeProps={{
className: 'font-bold underline',
}}
>
Home
</Link>
<Link
to="/packets"
className="hover:underline"
activeProps={{
className: 'font-bold underline',
}}
>
Packets
</Link>
</nav>
</div>
</div>
</header>
<main className="container mx-auto px-4 py-6">
<div className="flex min-h-screen bg-neutral-800">
{/* Sidebar Navigation */}
<Nav connectionStatus={connectionStatus} />
{/* Main Content Area */}
<main className="ml-64 flex-1 p-6">
<Outlet />
</main>
</div>

View File

@@ -1,17 +1,48 @@
import { InfoMessage, Separator } from "../components";
import { SITE_TITLE } from "../lib/config";
export function IndexPage() {
return (
<div>
<h2 className="text-2xl font-bold mb-4">Welcome to Meshstream</h2>
<p className="mb-4">
This application provides a real-time view of Meshtastic network traffic.
</p>
<div className="bg-blue-100 p-4 rounded">
<h3 className="text-lg font-semibold mb-2">Getting Started</h3>
<p>
Click on the <strong>Packets</strong> link in the navigation to view incoming
messages from the Meshtastic network.
<div className="bg-neutral-700 rounded-lg shadow-inner">
<div className="p-6">
<h2 className="text-2xl font-bold mb-4 text-neutral-100">
Welcome to {SITE_TITLE}
</h2>
<p className="mb-4 text-neutral-200">
This application provides a real-time view of Meshtastic network
traffic.
</p>
<InfoMessage
message="Click on the Packets link in the navigation to view incoming messages from the Meshtastic network."
type="info"
/>
</div>
<Separator className="my-6" />
<div className="p-6 grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-neutral-800 border border-neutral-700 p-5 rounded shadow-inner">
<h3 className="text-lg font-semibold mb-3 text-neutral-200">
About Meshtastic
</h3>
<p className="text-neutral-300">
Meshtastic is an open source, off-grid, decentralized mesh
communication platform. It allows devices to communicate without
cellular service or internet.
</p>
</div>
<div className="bg-neutral-800 border border-neutral-700 p-5 rounded shadow-inner">
<h3 className="text-lg font-semibold mb-3 text-neutral-200">
Data Privacy
</h3>
<p className="text-neutral-300">
All data is processed locally. Position data on public servers has
reduced precision for privacy protection.
</p>
</div>
</div>
</div>
);
}
}

View File

@@ -1,53 +1,47 @@
import { useEffect, useState } from 'react';
import { useAppDispatch } from '../hooks';
import { PacketList } from '../components/PacketList';
import { InfoMessage } from '../components/InfoMessage';
import { addPacket } from '../store/slices/packetSlice';
import { streamPackets, StreamEvent } from '../lib/api';
import { useEffect } from "react";
import { useAppDispatch } from "../hooks";
import { PacketList } from "../components/PacketList";
import { InfoMessage, Separator } from "../components";
import { addPacket } from "../store/slices/packetSlice";
import { streamPackets, StreamEvent } from "../lib/api";
export function PacketsRoute() {
const dispatch = useAppDispatch();
const [connectionStatus, setConnectionStatus] = useState<string>('Connecting...');
useEffect(() => {
// Set up Server-Sent Events connection using our API utility
const cleanup = streamPackets(
// Event handler for all event types
(event: StreamEvent) => {
if (event.type === 'info') {
// Handle info events (connection status, etc.)
setConnectionStatus(event.data);
} else if (event.type === 'message') {
if (event.type === "message") {
// Handle message events (actual packet data)
dispatch(addPacket(event.data));
}
},
// On error
() => {
setConnectionStatus('Connection error. Reconnecting...');
console.error('EventSource failed, reconnecting...');
}
);
// Clean up connection when component unmounts
return cleanup;
}, [dispatch]);
return (
<div>
<h2 className="text-2xl font-bold mb-4">Mesh Network Packets</h2>
{/* Connection status indicator */}
<InfoMessage
message={`Status: ${connectionStatus}`}
type={connectionStatus.includes('error') ? 'error' : 'info'}
/>
<p className="mb-4">
This page displays real-time packets from the Meshtastic mesh network.
</p>
<PacketList />
<div className="bg-neutral-700 rounded-lg shadow-inner">
<div className="p-6">
<h2 className="text-2xl font-bold mb-4 text-neutral-100">
Mesh Network Packets
</h2>
<InfoMessage
message="This page displays real-time packets from the Meshtastic mesh network."
type="info"
/>
</div>
<Separator className="my-2" />
<div className="p-6">
<PacketList />
</div>
</div>
);
}
}

View File

@@ -1,8 +1,8 @@
import { Link, Outlet } from '@tanstack/react-router';
import { Link, Outlet } from "@tanstack/react-router";
export function Root() {
return (
<div className="min-h-screen bg-gray-100">
<div className="min-h-screen bg-neutral-100">
<header className="bg-blue-600 text-white shadow-md">
<div className="container mx-auto px-4 py-3">
<div className="flex justify-between items-center">
@@ -12,7 +12,7 @@ export function Root() {
to="/"
className="hover:underline"
activeProps={{
className: 'font-bold underline',
className: "font-bold underline",
}}
>
Home
@@ -21,7 +21,7 @@ export function Root() {
to="/packets"
className="hover:underline"
activeProps={{
className: 'font-bold underline',
className: "font-bold underline",
}}
>
Packets

View File

@@ -1,5 +1,13 @@
@import "tailwindcss";
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Additional custom styles can be added here */
/* Set default background and text colors */
@layer base {
html, body {
@apply bg-neutral-800;
@apply text-neutral-200;
}
}

View File

@@ -1,11 +1,11 @@
const colors = require("tailwindcss/colors");
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
extend: { colors: { ...colors } },
},
darkMode: "class",
plugins: [],
};