mirror of
https://github.com/dpup/meshstream.git
synced 2026-03-28 17:42:37 +01:00
Shell styling
This commit is contained in:
@@ -48,7 +48,7 @@ export const ConnectionStatus: React.FC<ConnectionStatusProps> = ({
|
||||
const { icon, text, colorClass } = getStatusInfo();
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2 bg-neutral-800 p-4 border-inset">
|
||||
<div className="flex items-center space-x-2 p-4 rounded-xl bg-zinc-800 border border-neutral-950/80 effect-inset">
|
||||
{icon}
|
||||
<span className={cn("text-sm tracking-wide font-thin", colorClass)}>
|
||||
{text}
|
||||
|
||||
@@ -75,8 +75,6 @@ export const Nav: React.FC<NavProps> = ({ connectionStatus }) => {
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="px-4 py-2 pb-6">
|
||||
<ConnectionStatus status={connectionStatus} />
|
||||
</div>
|
||||
|
||||
@@ -2,45 +2,48 @@ import React, { useState, useEffect, 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, Radio } from "lucide-react";
|
||||
import { clearPackets, toggleStreamPause } from "../store/slices/packetSlice";
|
||||
import { Packet } from "../lib/types";
|
||||
import { Separator } from "./Separator";
|
||||
|
||||
// Number of packets to show per page
|
||||
const PACKETS_PER_PAGE = 10;
|
||||
|
||||
export const PacketList: React.FC = () => {
|
||||
const { packets, bufferedPackets, loading, error, streamPaused } = useAppSelector(
|
||||
(state) => state.packets
|
||||
);
|
||||
const { packets, bufferedPackets, loading, error, streamPaused } =
|
||||
useAppSelector((state) => state.packets);
|
||||
const dispatch = useAppDispatch();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
|
||||
// Generate a reproducible hash code for a string
|
||||
const hashString = useCallback((str: string): string => {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = (hash << 5) - hash + char;
|
||||
hash = hash & hash; // Convert to 32bit integer
|
||||
}
|
||||
// Make sure hash is always positive and convert to string
|
||||
return Math.abs(hash).toString(36);
|
||||
}, []);
|
||||
|
||||
|
||||
// Create a packet key using data.id and from address
|
||||
// This should match the key generation logic in the reducer
|
||||
const createPacketKey = useCallback((packet: Packet): string => {
|
||||
if (packet.data.id !== undefined && packet.data.from !== undefined) {
|
||||
// Use Meshtastic node ID format (! followed by lowercase hex) and packet ID
|
||||
const nodeId = `!${packet.data.from.toString(16).toLowerCase()}`;
|
||||
return `${nodeId}_${packet.data.id}`;
|
||||
} else {
|
||||
// Fallback to hash-based key if no ID or from (should be rare)
|
||||
return `hash_${hashString(JSON.stringify(packet))}`;
|
||||
}
|
||||
}, [hashString]);
|
||||
|
||||
const createPacketKey = useCallback(
|
||||
(packet: Packet): string => {
|
||||
if (packet.data.id !== undefined && packet.data.from !== undefined) {
|
||||
// Use Meshtastic node ID format (! followed by lowercase hex) and packet ID
|
||||
const nodeId = `!${packet.data.from.toString(16).toLowerCase()}`;
|
||||
return `${nodeId}_${packet.data.id}`;
|
||||
} else {
|
||||
// Fallback to hash-based key if no ID or from (should be rare)
|
||||
return `hash_${hashString(JSON.stringify(packet))}`;
|
||||
}
|
||||
},
|
||||
[hashString]
|
||||
);
|
||||
|
||||
// We don't need to track packet keys in state anymore since we use data.id
|
||||
// and it's deterministic - removing this effect to prevent the infinite loop issue
|
||||
|
||||
@@ -97,14 +100,10 @@ export const PacketList: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-xl font-normal text-neutral-200">
|
||||
Packets{" "}
|
||||
<span className="text-sm text-neutral-400">
|
||||
({packets.length} total)
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<div className="flex justify-between items-end mb-2">
|
||||
<div className="text-sm text-neutral-400 px-2">
|
||||
{packets.length} packets received, since 6:00am
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
{/* Show buffered count when paused */}
|
||||
{streamPaused && bufferedPackets.length > 0 && (
|
||||
@@ -123,13 +122,14 @@ export const PacketList: React.FC = () => {
|
||||
{/* Clear button */}
|
||||
<button
|
||||
onClick={handleClearPackets}
|
||||
className="flex items-center px-3 py-1.5 text-sm bg-neutral-700 hover:bg-neutral-600 rounded transition-colors text-neutral-200"
|
||||
className="flex items-center space-x-2 px-3 py-1.5 effect-outset border border-neutral-950/90 rounded-md text-neutral-400 hover:bg-neutral-700/50"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-1.5" />
|
||||
Clear
|
||||
<span className="text-sm font-medium">Clear</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="mx-0" />
|
||||
|
||||
<ul className="space-y-3">
|
||||
{currentPackets.map((packet, index) => (
|
||||
@@ -156,7 +156,9 @@ export const PacketList: React.FC = () => {
|
||||
{totalPages > 1 && (
|
||||
<div className="flex justify-between items-center mt-6 text-sm">
|
||||
<button
|
||||
onClick={() => setCurrentPage(currentPage > 1 ? currentPage - 1 : 1)}
|
||||
onClick={() =>
|
||||
setCurrentPage(currentPage > 1 ? currentPage - 1 : 1)
|
||||
}
|
||||
disabled={currentPage === 1}
|
||||
className={`px-3 py-1.5 rounded ${
|
||||
currentPage === 1
|
||||
@@ -190,4 +192,4 @@ export const PacketList: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,24 +2,15 @@ import React, { ReactNode } from "react";
|
||||
|
||||
interface PageWrapperProps {
|
||||
children: ReactNode;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A consistent page wrapper component for all leaf routes
|
||||
*/
|
||||
export const PageWrapper: React.FC<PageWrapperProps> = ({
|
||||
children,
|
||||
title,
|
||||
}) => {
|
||||
export const PageWrapper: React.FC<PageWrapperProps> = ({ children }) => {
|
||||
return (
|
||||
<div className="p-4 md:p-6">
|
||||
{title && (
|
||||
<h1 className="text-2xl font-medium text-neutral-100 mb-6">{title}</h1>
|
||||
)}
|
||||
<div className="bg-neutral-700 border-outset rounded-lg shadow-inner p-6">
|
||||
{children}
|
||||
</div>
|
||||
<div className="bg-neutral-50/5 rounded-tl-3xl rounded-bl-3xl p-4 shadow-md">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { cn } from "@/lib/cn";
|
||||
import React from "react";
|
||||
|
||||
interface SeparatorProps {
|
||||
@@ -7,7 +8,10 @@ interface SeparatorProps {
|
||||
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}`}
|
||||
className={cn(
|
||||
"mx-4 h-px border-t-1 border-t-neutral-950/80 border-b-1 border-b-[rgba(255,255,255,0.15)] my-3",
|
||||
className
|
||||
)}
|
||||
></div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ export const StreamControl: React.FC<StreamControlProps> = ({
|
||||
return (
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className={`flex items-center space-x-2 px-3 py-1.5 rounded-md transition-colors ${
|
||||
className={`flex items-center space-x-2 px-3 py-1.5 effect-outset border border-neutral-950/90 rounded-md transition-colors ${
|
||||
isPaused
|
||||
? "bg-neutral-700 text-amber-400 hover:bg-neutral-600"
|
||||
: "bg-neutral-700 text-green-400 hover:bg-neutral-600"
|
||||
@@ -22,11 +22,7 @@ export const StreamControl: React.FC<StreamControlProps> = ({
|
||||
<span className="text-sm font-medium">
|
||||
{isPaused ? "Paused" : "Streaming"}
|
||||
</span>
|
||||
{isPaused ? (
|
||||
<Play className="h-4 w-4" />
|
||||
) : (
|
||||
<Pause className="h-4 w-4" />
|
||||
)}
|
||||
{isPaused ? <Play className="h-4 w-4" /> : <Pause className="h-4 w-4" />}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ export const PacketCard: React.FC<PacketCardProps> = ({
|
||||
const { data } = packet;
|
||||
|
||||
return (
|
||||
<div className="p-4 border-inset">
|
||||
<div className="p-4 effect-inset bg-neutral-500/5 rounded-lg border border-neutral-950/60">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div className="flex items-center">
|
||||
<div className={`rounded-md ${iconBgColor} p-1.5 mr-3`}>{icon}</div>
|
||||
|
||||
@@ -28,14 +28,14 @@ export default function Root() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-neutral-800">
|
||||
<div className="flex min-h-screen bg-neutral-900">
|
||||
{/* Sidebar Navigation */}
|
||||
<Nav connectionStatus={connectionStatus} />
|
||||
|
||||
|
||||
{/* Main Content Area */}
|
||||
<main className="ml-64 flex-1 p-6">
|
||||
<main className="ml-64 flex-1 py-6">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { SITE_TITLE } from "../lib/config";
|
||||
|
||||
export function IndexPage() {
|
||||
return (
|
||||
<PageWrapper title={`Welcome to ${SITE_TITLE}`}>
|
||||
<PageWrapper>
|
||||
<div>
|
||||
<p className="mb-4 text-neutral-200">
|
||||
This application provides a real-time view of Meshtastic network
|
||||
@@ -42,4 +42,4 @@ export function IndexPage() {
|
||||
</div>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,4 +38,12 @@
|
||||
@apply rounded-md;
|
||||
@apply shadow-md;
|
||||
}
|
||||
.effect-outset {
|
||||
@apply shadow-inner;
|
||||
@apply [box-shadow:inset_0_1px_0_0_rgba(255,255,255,0.15),inset_0_-1px_0_0_rgba(0,0,0,0.3)];
|
||||
}
|
||||
.effect-inset {
|
||||
@apply shadow-inner;
|
||||
@apply [box-shadow:inset_0_1px_2px_0_rgba(0,0,0,0.3),inset_0_-1px_1px_0_rgba(255,255,255,0.15)];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user