mirror of
https://github.com/dpup/meshstream.git
synced 2026-06-12 10:14:44 +02:00
187 lines
5.1 KiB
TypeScript
187 lines
5.1 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import { Link } from "@tanstack/react-router";
|
|
import { ConnectionStatus } from "./ConnectionStatus";
|
|
import { Separator } from "./Separator";
|
|
import { Button } from "./ui/Button";
|
|
import { SITE_TITLE } from "../lib/config";
|
|
import {
|
|
Info,
|
|
Layers,
|
|
LayoutDashboard,
|
|
Radio,
|
|
MessageSquare,
|
|
Map,
|
|
LucideIcon,
|
|
Menu,
|
|
X,
|
|
} from "lucide-react";
|
|
|
|
// Define navigation item structure
|
|
interface NavItem {
|
|
to: string;
|
|
label: string;
|
|
icon: LucideIcon;
|
|
exact?: boolean;
|
|
}
|
|
|
|
// Define props for the component
|
|
interface NavProps {
|
|
connectionStatus: string;
|
|
}
|
|
|
|
// Navigation items data
|
|
const navigationItems: NavItem[] = [
|
|
{
|
|
to: "/home",
|
|
label: "Dashboard",
|
|
icon: LayoutDashboard,
|
|
exact: true,
|
|
},
|
|
{
|
|
to: "/map",
|
|
label: "Network Map",
|
|
icon: Map,
|
|
exact: true,
|
|
},
|
|
{
|
|
to: "/stream",
|
|
label: "Stream",
|
|
icon: Radio,
|
|
exact: true,
|
|
},
|
|
{
|
|
to: "/channels",
|
|
label: "Messages",
|
|
icon: MessageSquare,
|
|
},
|
|
{
|
|
to: "/info",
|
|
label: "Information",
|
|
icon: Info,
|
|
exact: true,
|
|
},
|
|
];
|
|
|
|
export const Nav: React.FC<NavProps> = ({ connectionStatus }) => {
|
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
|
const [isMobileView, setIsMobileView] = useState(false);
|
|
|
|
// Handle window resize to determine if we're in mobile view
|
|
useEffect(() => {
|
|
const handleResize = () => {
|
|
setIsMobileView(window.innerWidth < 768);
|
|
if (window.innerWidth >= 768) {
|
|
setIsMobileMenuOpen(false);
|
|
}
|
|
};
|
|
|
|
// Set initial state
|
|
handleResize();
|
|
|
|
// Add event listener
|
|
window.addEventListener("resize", handleResize);
|
|
|
|
// Clean up
|
|
return () => window.removeEventListener("resize", handleResize);
|
|
}, []);
|
|
|
|
// Navigation links component to avoid duplication across desktop and mobile
|
|
const NavLinks = () => (
|
|
<ul className="space-y-1">
|
|
{navigationItems.map((item) => (
|
|
<li key={item.to}>
|
|
<Link
|
|
to={item.to}
|
|
className="flex items-center px-4 py-2.5 transition-colors"
|
|
onClick={() => isMobileView && setIsMobileMenuOpen(false)}
|
|
inactiveProps={{
|
|
className:
|
|
"text-neutral-400 hover:text-neutral-200 font-thin",
|
|
}}
|
|
activeProps={{
|
|
className: "text-neutral-200 font-normal",
|
|
}}
|
|
activeOptions={{
|
|
exact: item.exact,
|
|
}}
|
|
>
|
|
<item.icon className="h-4 w-4 mr-3" />
|
|
{item.label}
|
|
</Link>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
);
|
|
|
|
return (
|
|
<>
|
|
{/* Mobile header (always visible on mobile) */}
|
|
<div className="md:hidden fixed top-0 left-0 right-0 z-20 border-neutral-700 border-b shadow-inner bg-neutral-800/40">
|
|
<div className="flex items-center justify-between p-3">
|
|
<div className="flex items-center">
|
|
<div className="bg-pink-400 rounded-md mr-3 p-1.5 flex items-center justify-center">
|
|
<Layers className="h-5 w-5 text-neutral-800" />
|
|
</div>
|
|
<h1 className="text-xl font-thin tracking-wide text-neutral-100">{SITE_TITLE}</h1>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
icon={isMobileMenuOpen ? X : Menu}
|
|
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
|
aria-label={isMobileMenuOpen ? "Close menu" : "Open menu"}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile menu overlay */}
|
|
{isMobileMenuOpen && (
|
|
<div
|
|
className="fixed inset-0 bg-black/50 z-30 md:hidden"
|
|
onClick={() => setIsMobileMenuOpen(false)}
|
|
/>
|
|
)}
|
|
|
|
{/* Mobile menu */}
|
|
<aside
|
|
className={`
|
|
fixed top-0 right-0 h-screen bg-neutral-800 z-40 w-64 transform transition-transform duration-300 ease-in-out md:hidden
|
|
${isMobileMenuOpen ? 'translate-x-0' : 'translate-x-full'}
|
|
`}
|
|
>
|
|
<div className="flex flex-col h-full pt-16">
|
|
<nav className="flex-1 overflow-y-auto">
|
|
<NavLinks />
|
|
</nav>
|
|
<div className="px-4 py-2 pb-6">
|
|
<ConnectionStatus status={connectionStatus} />
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
{/* Desktop sidebar (hidden on mobile) */}
|
|
<aside className="hidden md:flex w-64 text-neutral-100 h-screen fixed left-0 top-0 flex-col">
|
|
{/* Logo and title section */}
|
|
<div className="p-4 flex items-center">
|
|
<div className="bg-pink-400 rounded-md mr-3 p-1.5 flex items-center justify-center">
|
|
<Layers className="h-5 w-5 text-neutral-800" />
|
|
</div>
|
|
<h1 className="text-xl font-thin tracking-wide">{SITE_TITLE}</h1>
|
|
</div>
|
|
|
|
<Separator className="mt-1" />
|
|
|
|
<nav className="flex-1 overflow-y-auto">
|
|
<NavLinks />
|
|
</nav>
|
|
|
|
<div className="px-4 py-2 pb-6">
|
|
<ConnectionStatus status={connectionStatus} />
|
|
</div>
|
|
</aside>
|
|
|
|
{/* Content padding spacer for mobile view - this will be positioned outside the main content area */}
|
|
<div className="md:hidden h-16"></div>
|
|
</>
|
|
);
|
|
};
|