- Add useTranslation() to TopNav, BottomTabBar, FabMenu, UserMenu - Internationalize ConfirmDialog, AuthPromptModal, ExternalLinkDialog - Extract all onboarding flow strings (Welcome, HobbyPicker, ItemBrowser, Review, Done) - Internationalize settings page (weight unit, currency, API keys, import/export) - Internationalize login page and root error boundary - All dialogs in __root.tsx use t() for UI chrome Phase 34, Plan 02 (core navigation and global UI)
151 lines
4.3 KiB
TypeScript
151 lines
4.3 KiB
TypeScript
import { Link, useMatchRoute, useNavigate } from "@tanstack/react-router";
|
|
import { useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
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 { t } = useTranslation();
|
|
const { data: auth } = useAuth();
|
|
const isAuthenticated = !!auth?.user;
|
|
const openAuthPrompt = useUIStore((s) => s.openAuthPrompt);
|
|
const matchRoute = useMatchRoute();
|
|
const navigate = useNavigate();
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
|
|
const isHome = !!matchRoute({ to: "/" });
|
|
const isCollection = !!matchRoute({ to: "/collection", fuzzy: true });
|
|
const isSetups = !!matchRoute({ to: "/setups", fuzzy: true });
|
|
|
|
function handleSearch() {
|
|
const trimmed = searchQuery.trim();
|
|
if (!trimmed) return;
|
|
navigate({ to: "/global-items", search: { q: trimmed } });
|
|
setSearchQuery("");
|
|
}
|
|
|
|
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}
|
|
>
|
|
{t("nav.home")}
|
|
</NavLinkOrButton>
|
|
<NavLinkOrButton
|
|
to="/collection"
|
|
isActive={isCollection}
|
|
isProtected={true}
|
|
isAuthenticated={isAuthenticated}
|
|
onAuthPrompt={openAuthPrompt}
|
|
>
|
|
{t("nav.collection")}
|
|
</NavLinkOrButton>
|
|
<NavLinkOrButton
|
|
to="/setups"
|
|
isActive={isSetups}
|
|
isProtected={true}
|
|
isAuthenticated={isAuthenticated}
|
|
onAuthPrompt={openAuthPrompt}
|
|
>
|
|
{t("nav.setups")}
|
|
</NavLinkOrButton>
|
|
</nav>
|
|
|
|
{/* Right: Search bar (desktop only) + User section */}
|
|
<div className="flex items-center gap-3">
|
|
{/* Search bar — desktop only */}
|
|
<div className="relative hidden md:flex items-center">
|
|
<button
|
|
type="button"
|
|
onClick={handleSearch}
|
|
className="absolute left-2.5 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
|
|
tabIndex={-1}
|
|
aria-label="Search"
|
|
>
|
|
<LucideIcon name="search" size={16} />
|
|
</button>
|
|
<input
|
|
type="text"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter") handleSearch();
|
|
}}
|
|
placeholder={t("nav.searchPlaceholder")}
|
|
className="bg-gray-50 border border-gray-200 rounded-lg pl-9 pr-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-200 focus:border-gray-300 w-48 lg:w-64 transition-colors"
|
|
/>
|
|
</div>
|
|
|
|
{/* User section */}
|
|
{isAuthenticated ? (
|
|
<UserMenu />
|
|
) : (
|
|
<Link
|
|
to="/login"
|
|
className="text-xs text-gray-500 hover:text-gray-700 transition-colors"
|
|
>
|
|
{t("auth.signIn")}
|
|
</Link>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|