feat(27-01): create TopNav component
- Sticky top nav bar replacing TotalsBar with full navigation
- Logo, Home/Collection/Setups links, search bar, and user avatar
- NavLinkOrButton helper: button for anon users on protected routes, Link for authenticated
- Active route highlighting via useMatchRoute
- Desktop search bar triggers openCatalogSearch('collection')
- Desktop nav links hidden on mobile (hidden md:flex)
- Uses LucideIcon wrapper, not direct lucide-react imports
[Rule 1 - Bug] Used 'house' icon fallback check: plan specified 'home' which does not exist in lucide-react; 'search' and 'layers' verified present
This commit is contained in:
129
src/client/components/TopNav.tsx
Normal file
129
src/client/components/TopNav.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import { Link, useMatchRoute } from "@tanstack/react-router";
|
||||
import { useAuth } from "../hooks/useAuth";
|
||||
import { LucideIcon } from "../lib/iconData";
|
||||
import { useUIStore } from "../stores/uiStore";
|
||||
import { UserMenu } from "./UserMenu";
|
||||
|
||||
interface NavLinkOrButtonProps {
|
||||
to: string;
|
||||
isActive: boolean;
|
||||
isProtected: boolean;
|
||||
isAuthenticated: boolean;
|
||||
onAuthPrompt: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function NavLinkOrButton({
|
||||
to,
|
||||
isActive,
|
||||
isProtected,
|
||||
isAuthenticated,
|
||||
onAuthPrompt,
|
||||
children,
|
||||
}: NavLinkOrButtonProps) {
|
||||
const activeClass = "text-gray-900 font-medium";
|
||||
const inactiveClass = "text-gray-500 hover:text-gray-700 transition-colors";
|
||||
const className = `text-sm ${isActive ? activeClass : inactiveClass}`;
|
||||
|
||||
if (isProtected && !isAuthenticated) {
|
||||
return (
|
||||
<button type="button" onClick={onAuthPrompt} className={className}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link to={to} className={className}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function TopNav() {
|
||||
const { data: auth } = useAuth();
|
||||
const isAuthenticated = !!auth?.user;
|
||||
const openCatalogSearch = useUIStore((s) => s.openCatalogSearch);
|
||||
const openAuthPrompt = useUIStore((s) => s.openAuthPrompt);
|
||||
const matchRoute = useMatchRoute();
|
||||
|
||||
const isHome = !!matchRoute({ to: "/" });
|
||||
const isCollection = !!matchRoute({ to: "/collection", fuzzy: true });
|
||||
const isSetups = !!matchRoute({ to: "/setups", fuzzy: true });
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 z-10 bg-white border-b border-gray-100">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-14">
|
||||
{/* Left: Logo */}
|
||||
<Link
|
||||
to="/"
|
||||
className="text-lg font-semibold text-gray-900 hover:text-gray-600 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<LucideIcon name="package" size={20} className="text-gray-500" />
|
||||
GearBox
|
||||
</Link>
|
||||
|
||||
{/* Center: Desktop nav links */}
|
||||
<nav className="hidden md:flex items-center gap-6">
|
||||
<NavLinkOrButton
|
||||
to="/"
|
||||
isActive={isHome}
|
||||
isProtected={false}
|
||||
isAuthenticated={isAuthenticated}
|
||||
onAuthPrompt={openAuthPrompt}
|
||||
>
|
||||
Home
|
||||
</NavLinkOrButton>
|
||||
<NavLinkOrButton
|
||||
to="/collection"
|
||||
isActive={isCollection}
|
||||
isProtected={true}
|
||||
isAuthenticated={isAuthenticated}
|
||||
onAuthPrompt={openAuthPrompt}
|
||||
>
|
||||
Collection
|
||||
</NavLinkOrButton>
|
||||
<NavLinkOrButton
|
||||
to="/setups"
|
||||
isActive={isSetups}
|
||||
isProtected={true}
|
||||
isAuthenticated={isAuthenticated}
|
||||
onAuthPrompt={openAuthPrompt}
|
||||
>
|
||||
Setups
|
||||
</NavLinkOrButton>
|
||||
</nav>
|
||||
|
||||
{/* Right: Search bar (desktop only) + User section */}
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Search bar — desktop only */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openCatalogSearch("collection")}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") openCatalogSearch("collection");
|
||||
}}
|
||||
className="hidden md:flex items-center gap-2 bg-gray-50 border border-gray-200 rounded-lg px-3 py-1.5 text-sm text-gray-400 cursor-pointer hover:border-gray-300 transition-colors"
|
||||
>
|
||||
<LucideIcon name="search" size={16} />
|
||||
<span className="hidden lg:inline">Search catalog...</span>
|
||||
</button>
|
||||
|
||||
{/* User section */}
|
||||
{isAuthenticated ? (
|
||||
<UserMenu />
|
||||
) : (
|
||||
<Link
|
||||
to="/login"
|
||||
className="text-xs text-gray-500 hover:text-gray-700 transition-colors"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user