From ca231c81bd3bc53e0f03403a2f5b32240e348eea Mon Sep 17 00:00:00 2001 From: Daniel Pupius Date: Fri, 25 Apr 2025 17:30:02 -0700 Subject: [PATCH] Add Button component --- web/src/components/PacketList.tsx | 55 ++++++++++---------- web/src/components/ui/Button.tsx | 72 ++++++++++++++++++++++++++ web/src/routes/channel.$channelId.tsx | 73 +++++++++++++++++---------- web/src/routes/channels.tsx | 32 +++++++----- web/src/routes/home.tsx | 11 +--- 5 files changed, 169 insertions(+), 74 deletions(-) create mode 100644 web/src/components/ui/Button.tsx diff --git a/web/src/components/PacketList.tsx b/web/src/components/PacketList.tsx index e9a7bde..e9e7994 100644 --- a/web/src/components/PacketList.tsx +++ b/web/src/components/PacketList.tsx @@ -2,10 +2,17 @@ import React, { useState, useCallback } from "react"; import { useAppSelector, useAppDispatch } from "../hooks"; import { PacketRenderer } from "./packets/PacketRenderer"; import { StreamControl } from "./StreamControl"; -import { Trash2, RefreshCw, Archive } from "lucide-react"; +import { + Trash2, + RefreshCw, + Archive, + ChevronLeft, + ChevronRight, +} from "lucide-react"; import { clearPackets, toggleStreamPause } from "../store/slices/packetSlice"; import { Packet } from "../lib/types"; import { Separator } from "./Separator"; +import { Button } from "./ui/Button"; // Number of packets to show per page const PACKETS_PER_PAGE = 100; @@ -122,13 +129,9 @@ export const PacketList: React.FC = () => { /> {/* Clear button */} - +
{packets.length} packets received @@ -183,12 +186,14 @@ export const PacketList: React.FC = () => { messages.
- + )} @@ -202,35 +207,31 @@ export const PacketList: React.FC = () => {
- + Previous + - + Next +
diff --git a/web/src/components/ui/Button.tsx b/web/src/components/ui/Button.tsx new file mode 100644 index 0000000..5d049dd --- /dev/null +++ b/web/src/components/ui/Button.tsx @@ -0,0 +1,72 @@ +import React from "react"; +import { LucideIcon } from "lucide-react"; +import { cn } from "../../lib/cn"; + +export interface ButtonProps extends React.ButtonHTMLAttributes { + /** Button content */ + children: React.ReactNode; + /** Optional icon to display before the text */ + icon?: LucideIcon; + /** Button variant */ + variant?: "primary" | "secondary" | "danger" | "ghost"; + /** Button size */ + size?: "sm" | "md" | "lg"; + /** Full width button */ + fullWidth?: boolean; + /** Optional class name to extend styles */ + className?: string; +} + +export const Button = React.forwardRef( + ( + { + children, + icon: Icon, + variant = "secondary", + size = "md", + fullWidth = false, + className, + disabled, + ...props + }, + ref + ) => { + const variantClasses = { + primary: "bg-blue-600 text-white hover:bg-blue-700 border-blue-700/50", + secondary: "text-neutral-400 hover:bg-neutral-700/50 border-neutral-950/90", + danger: "bg-red-700/20 text-red-400 hover:bg-red-700/30 border-red-900/60", + ghost: "text-neutral-400 hover:bg-neutral-700/30 border-transparent", + }; + + const sizeClasses = { + sm: "px-2 py-1 text-xs", + md: "px-3 py-1.5 text-sm", + lg: "px-4 py-2 text-base", + }; + + const disabledClasses = disabled + ? "opacity-50 cursor-not-allowed" + : "transition-colors"; + + return ( + + ); + } +); + +Button.displayName = "Button"; \ No newline at end of file diff --git a/web/src/routes/channel.$channelId.tsx b/web/src/routes/channel.$channelId.tsx index 8400c82..83eab7b 100644 --- a/web/src/routes/channel.$channelId.tsx +++ b/web/src/routes/channel.$channelId.tsx @@ -4,45 +4,59 @@ import { PageWrapper, MessageBubble } from "../components"; import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; import { ChevronLeft } from "lucide-react"; -export const Route = createFileRoute('/channel/$channelId')({ +export const Route = createFileRoute("/channel/$channelId")({ component: ChannelPage, }); // Generate a deterministic color based on channel ID const getChannelColor = (channelId: string) => { const colors = [ - 'bg-green-500', 'bg-blue-500', 'bg-amber-500', 'bg-purple-500', - 'bg-pink-500', 'bg-indigo-500', 'bg-red-500', 'bg-teal-500' + "bg-green-500", + "bg-blue-500", + "bg-amber-500", + "bg-purple-500", + "bg-pink-500", + "bg-indigo-500", + "bg-red-500", + "bg-teal-500", ]; - + // Use a hash of the channel ID to pick a consistent color - const hash = channelId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0); + const hash = channelId + .split("") + .reduce((acc, char) => acc + char.charCodeAt(0), 0); return colors[hash % colors.length]; }; function ChannelPage() { const { channelId } = Route.useParams(); const navigate = useNavigate(); - - const { channels, messages, nodes } = useAppSelector(state => state.aggregator); + + const { channels, messages, nodes } = useAppSelector( + (state) => state.aggregator + ); const channel = channels[channelId]; // Create the channel key in the same format used by the aggregator const channelKey = `channel_${channelId}`; - + // Get channel messages and sort by timestamp (oldest to newest) // This creates a shallow copy of the message array to sort it without modifying the original - const channelMessages = messages[channelKey] ? - [...messages[channelKey]].sort((a, b) => a.timestamp - b.timestamp) : - []; - + const channelMessages = messages[channelKey] + ? [...messages[channelKey]].sort((a, b) => a.timestamp - b.timestamp) + : []; + useEffect(() => { if (!channel) { console.log(`[Channel] Channel ${channelId} not found`); // Navigate back to channels page if this channel doesn't exist - setTimeout(() => navigate({ to: '/channels' }), 500); + setTimeout(() => navigate({ to: "/channels" }), 500); } else { - console.log(`[Channel] Displaying ${channelMessages.length} messages for channel ${channelId}`); - console.log(`[Channel] Message count from channel data: ${channel.messageCount}`); + console.log( + `[Channel] Displaying ${channelMessages.length} messages for channel ${channelId}` + ); + console.log( + `[Channel] Message count from channel data: ${channel.messageCount}` + ); console.log(`[Channel] Available messages:`, messages); console.log(`[Channel] Looking for key:`, channelKey); } @@ -63,28 +77,33 @@ function ChannelPage() { return ( -
+
{/* Header */}
-
-
+
{channelId.substring(0, 2).toUpperCase()}
-

{channelId}

+

+ {channelId} +

- {channel.nodes.length} nodes · {channel.textMessageCount} messages + {channel.nodes.length} nodes · {channel.textMessageCount}{" "} + messages

- + {/* Message List */}
{channelMessages.length === 0 ? ( @@ -94,10 +113,12 @@ function ChannelPage() { ) : (
{channelMessages.map((message) => { - const nodeName = nodes[message.from]?.shortName || nodes[message.from]?.longName; - + const nodeName = + nodes[message.from]?.shortName || + nodes[message.from]?.longName; + return ( - ); -} \ No newline at end of file +} diff --git a/web/src/routes/channels.tsx b/web/src/routes/channels.tsx index 702e064..207a6e1 100644 --- a/web/src/routes/channels.tsx +++ b/web/src/routes/channels.tsx @@ -3,22 +3,29 @@ import { useAppSelector } from "../hooks"; import { PageWrapper, ChannelCard } from "../components"; import { createFileRoute } from "@tanstack/react-router"; -export const Route = createFileRoute('/channels')({ +export const Route = createFileRoute("/channels")({ component: ChannelsPage, }); function ChannelsPage() { - const { channels, messages } = useAppSelector(state => state.aggregator); + const { channels, messages } = useAppSelector((state) => state.aggregator); useEffect(() => { - console.log(`[Channels] Displaying ${Object.keys(channels).length} channels`); + console.log( + `[Channels] Displaying ${Object.keys(channels).length} channels` + ); console.log(`[Channels] Messages in store:`, messages); - + // Check if any channels have text messages but none are displayed Object.entries(channels).forEach(([channelId, channel]) => { const channelKey = `channel_${channelId}`; - if (channel.textMessageCount > 0 && (!messages[channelKey] || messages[channelKey].length === 0)) { - console.log(`[Channels] Mismatch: Channel ${channelId} reports ${channel.textMessageCount} text messages but has ${messages[channelKey]?.length || 0} in store`); + if ( + channel.textMessageCount > 0 && + (!messages[channelKey] || messages[channelKey].length === 0) + ) { + console.log( + `[Channels] Mismatch: Channel ${channelId} reports ${channel.textMessageCount} text messages but has ${messages[channelKey]?.length || 0} in store` + ); } }); }, [channels, messages]); @@ -32,20 +39,21 @@ function ChannelsPage() { // If one has activity and other doesn't, prioritize the active one if (a.lastMessage) return -1; if (b.lastMessage) return 1; - + // Fall back to text message count if no last message time return (b.textMessageCount || 0) - (a.textMessageCount || 0); }); return ( -
-

Channels

- +
{sortedChannels.length === 0 ? (

No channels discovered yet.

-

Channels will appear here as they are discovered on the Meshtastic network.

+

+ Channels will appear here as they are discovered on the Meshtastic + network. +

) : (
@@ -57,4 +65,4 @@ function ChannelsPage() {
); -} \ No newline at end of file +} diff --git a/web/src/routes/home.tsx b/web/src/routes/home.tsx index 3b99fcc..2793161 100644 --- a/web/src/routes/home.tsx +++ b/web/src/routes/home.tsx @@ -1,25 +1,18 @@ import { PageWrapper, NodeList, GatewayList } from "../components"; import { createFileRoute } from "@tanstack/react-router"; -export const Route = createFileRoute('/home')({ +export const Route = createFileRoute("/home")({ component: HomePage, }); function HomePage() { return ( -
-

Mesh Overview

-

- Real-time view of your Meshtastic mesh network traffic -

-
-
- +