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
+55
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>
);
};
+6 -3
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)}
+36 -20
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>
);
};
};
+32 -12
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>
);
+57
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>
);
};
+3 -3
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>
+14 -9
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>
))}
+13
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>
);
};
+3
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';
+4 -1
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`,
};
};
+33 -30
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>
+42 -11
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>
);
}
}
+26 -32
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>
);
}
}
+4 -4
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
+9 -1
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;
}
}