From 842919bce5512c9f9aa771e99e8717208291cc4d Mon Sep 17 00:00:00 2001 From: Felipe Coutinho Date: Fri, 27 Feb 2026 15:40:48 +0000 Subject: [PATCH] refactor: substituir topbar por navbar componentizada Co-Authored-By: Claude Opus 4.6 --- app/(dashboard)/layout.tsx | 4 +- .../app-topbar.tsx => navbar/app-navbar.tsx} | 16 +- .../mobile-link.tsx} | 16 +- .../nav-dropdown.tsx} | 23 +- components/navbar/nav-items.tsx | 113 +++++++ components/navbar/nav-link.tsx | 30 ++ components/navbar/nav-menu.tsx | 117 +++++++ .../nav-pill.tsx} | 13 +- components/{topbar => navbar}/nav-styles.ts | 0 .../nav-tools.tsx} | 8 +- .../navbar-user.tsx} | 4 +- components/topbar/top-nav-menu.tsx | 302 ------------------ 12 files changed, 303 insertions(+), 343 deletions(-) rename components/{topbar/app-topbar.tsx => navbar/app-navbar.tsx} (81%) rename components/{topbar/mobile-nav-link.tsx => navbar/mobile-link.tsx} (84%) rename components/{topbar/dropdown-link-list.tsx => navbar/nav-dropdown.tsx} (69%) create mode 100644 components/navbar/nav-items.tsx create mode 100644 components/navbar/nav-link.tsx create mode 100644 components/navbar/nav-menu.tsx rename components/{topbar/simple-nav-link.tsx => navbar/nav-pill.tsx} (68%) rename components/{topbar => navbar}/nav-styles.ts (100%) rename components/{topbar/ferramentas-dropdown.tsx => navbar/nav-tools.tsx} (94%) rename components/{topbar/topbar-user.tsx => navbar/navbar-user.tsx} (97%) delete mode 100644 components/topbar/top-nav-menu.tsx diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx index 9558863..62d747a 100644 --- a/app/(dashboard)/layout.tsx +++ b/app/(dashboard)/layout.tsx @@ -1,6 +1,6 @@ import { FontProvider } from "@/components/font-provider"; +import { AppNavbar } from "@/components/navbar/app-navbar"; import { PrivacyProvider } from "@/components/privacy-provider"; -import { AppTopbar } from "@/components/topbar/app-topbar"; import { getUserSession } from "@/lib/auth/server"; import { fetchDashboardNotifications } from "@/lib/dashboard/notifications"; import { fetchPagadoresWithAccess } from "@/lib/pagadores/access"; @@ -53,7 +53,7 @@ export default async function DashboardLayout({ moneyFont={fontPrefs.moneyFont} > - +
{/* Logo */} @@ -34,7 +34,7 @@ export function AppTopbar({ {/* Navigation */} - + {/* Right-side actions */}
@@ -49,7 +49,7 @@ export function AppTopbar({
{/* User avatar */} - +
); diff --git a/components/topbar/mobile-nav-link.tsx b/components/navbar/mobile-link.tsx similarity index 84% rename from components/topbar/mobile-nav-link.tsx rename to components/navbar/mobile-link.tsx index 1deb761..bb43791 100644 --- a/components/topbar/mobile-nav-link.tsx +++ b/components/navbar/mobile-link.tsx @@ -1,34 +1,38 @@ "use client"; -import Link from "next/link"; import { usePathname } from "next/navigation"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils/ui"; +import { NavLink } from "./nav-link"; -type MobileNavLinkProps = { +type MobileLinkProps = { href: string; icon: React.ReactNode; children: React.ReactNode; onClick?: () => void; badge?: number; + preservePeriod?: boolean; }; -export function MobileNavLink({ +export function MobileLink({ href, icon, children, onClick, badge, -}: MobileNavLinkProps) { + preservePeriod, +}: MobileLinkProps) { const pathname = usePathname(); + const isActive = href === "/dashboard" ? pathname === href : pathname === href || pathname.startsWith(`${href}/`); return ( - ) : null} - + ); } diff --git a/components/topbar/dropdown-link-list.tsx b/components/navbar/nav-dropdown.tsx similarity index 69% rename from components/topbar/dropdown-link-list.tsx rename to components/navbar/nav-dropdown.tsx index 5fc350b..5b4cba9 100644 --- a/components/topbar/dropdown-link-list.tsx +++ b/components/navbar/nav-dropdown.tsx @@ -1,24 +1,21 @@ -import Link from "next/link"; +"use client"; + import { Badge } from "@/components/ui/badge"; +import type { NavItem } from "./nav-items"; +import { NavLink } from "./nav-link"; -export type DropdownLinkItem = { - href: string; - label: string; - icon: React.ReactNode; - badge?: number; +type NavDropdownProps = { + items: NavItem[]; }; -type DropdownLinkListProps = { - items: DropdownLinkItem[]; -}; - -export function DropdownLinkList({ items }: DropdownLinkListProps) { +export function NavDropdown({ items }: NavDropdownProps) { return (
    {items.map((item) => (
  • - {item.icon} @@ -31,7 +28,7 @@ export function DropdownLinkList({ items }: DropdownLinkListProps) { {item.badge} ) : null} - +
  • ))}
diff --git a/components/navbar/nav-items.tsx b/components/navbar/nav-items.tsx new file mode 100644 index 0000000..846ba09 --- /dev/null +++ b/components/navbar/nav-items.tsx @@ -0,0 +1,113 @@ +import { + RiArrowLeftRightLine, + RiBankCard2Line, + RiBankLine, + RiCalendarEventLine, + RiFileChartLine, + RiFundsLine, + RiGroupLine, + RiInboxLine, + RiPriceTag3Line, + RiSparklingLine, + RiTodoLine, +} from "@remixicon/react"; + +export type NavItem = { + href: string; + label: string; + icon: React.ReactNode; + badge?: number; + preservePeriod?: boolean; +}; + +export type NavSection = { + label: string; + items: NavItem[]; +}; + +export const NAV_SECTIONS: NavSection[] = [ + { + label: "Lançamentos", + items: [ + { + href: "/lancamentos", + label: "lançamentos", + icon: , + preservePeriod: true, + }, + { + href: "/pre-lancamentos", + label: "pré-lançamentos", + icon: , + }, + { + href: "/calendario", + label: "calendário", + icon: , + }, + ], + }, + { + label: "Finanças", + items: [ + { + href: "/cartoes", + label: "cartões", + icon: , + }, + { + href: "/contas", + label: "contas", + icon: , + }, + { + href: "/orcamentos", + label: "orçamentos", + icon: , + preservePeriod: true, + }, + ], + }, + { + label: "Organização", + items: [ + { + href: "/pagadores", + label: "pagadores", + icon: , + }, + { + href: "/categorias", + label: "categorias", + icon: , + }, + { + href: "/anotacoes", + label: "anotações", + icon: , + }, + ], + }, + { + label: "Relatórios", + items: [ + { + href: "/insights", + label: "insights", + icon: , + preservePeriod: true, + }, + { + href: "/relatorios/tendencias", + label: "tendências", + icon: , + }, + { + href: "/relatorios/uso-cartoes", + label: "uso de cartões", + icon: , + preservePeriod: true, + }, + ], + }, +]; diff --git a/components/navbar/nav-link.tsx b/components/navbar/nav-link.tsx new file mode 100644 index 0000000..aa41a01 --- /dev/null +++ b/components/navbar/nav-link.tsx @@ -0,0 +1,30 @@ +"use client"; + +import Link from "next/link"; +import { useSearchParams } from "next/navigation"; +import { useMemo } from "react"; + +const PERIOD_PARAM = "periodo"; + +type NavLinkProps = Omit, "href"> & { + href: string; + preservePeriod?: boolean; +}; + +export function NavLink({ + href, + preservePeriod = false, + ...props +}: NavLinkProps) { + const searchParams = useSearchParams(); + + const resolvedHref = useMemo(() => { + if (!preservePeriod) return href; + const periodo = searchParams.get(PERIOD_PARAM); + if (!periodo) return href; + const separator = href.includes("?") ? "&" : "?"; + return `${href}${separator}${PERIOD_PARAM}=${encodeURIComponent(periodo)}`; + }, [href, preservePeriod, searchParams]); + + return ; +} diff --git a/components/navbar/nav-menu.tsx b/components/navbar/nav-menu.tsx new file mode 100644 index 0000000..212fff9 --- /dev/null +++ b/components/navbar/nav-menu.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { RiDashboardLine, RiMenuLine } from "@remixicon/react"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + NavigationMenu, + NavigationMenuContent, + NavigationMenuItem, + NavigationMenuList, + NavigationMenuTrigger, +} from "@/components/ui/navigation-menu"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; +import { MobileLink, MobileSectionLabel } from "./mobile-link"; +import { NavDropdown } from "./nav-dropdown"; +import { NAV_SECTIONS } from "./nav-items"; +import { NavPill } from "./nav-pill"; +import { triggerClass } from "./nav-styles"; +import { MobileTools, NavToolsDropdown } from "./nav-tools"; + +export function NavMenu() { + const [sheetOpen, setSheetOpen] = useState(false); + const close = () => setSheetOpen(false); + + return ( + <> + {/* Desktop */} + + + {/* Mobile */} + + + + + + + Menu + + + + + + ); +} diff --git a/components/topbar/simple-nav-link.tsx b/components/navbar/nav-pill.tsx similarity index 68% rename from components/topbar/simple-nav-link.tsx rename to components/navbar/nav-pill.tsx index 4f2a6a2..b2e5e6a 100644 --- a/components/topbar/simple-nav-link.tsx +++ b/components/navbar/nav-pill.tsx @@ -1,28 +1,31 @@ "use client"; -import Link from "next/link"; import { usePathname } from "next/navigation"; import { cn } from "@/lib/utils/ui"; +import { NavLink } from "./nav-link"; import { linkActive, linkBase, linkIdle } from "./nav-styles"; -type SimpleNavLinkProps = { +type NavPillProps = { href: string; + preservePeriod?: boolean; children: React.ReactNode; }; -export function SimpleNavLink({ href, children }: SimpleNavLinkProps) { +export function NavPill({ href, preservePeriod, children }: NavPillProps) { const pathname = usePathname(); + const isActive = href === "/dashboard" ? pathname === href : pathname === href || pathname.startsWith(`${href}/`); return ( - {children} - + ); } diff --git a/components/topbar/nav-styles.ts b/components/navbar/nav-styles.ts similarity index 100% rename from components/topbar/nav-styles.ts rename to components/navbar/nav-styles.ts diff --git a/components/topbar/ferramentas-dropdown.tsx b/components/navbar/nav-tools.tsx similarity index 94% rename from components/topbar/ferramentas-dropdown.tsx rename to components/navbar/nav-tools.tsx index bbd5c62..55c98a6 100644 --- a/components/topbar/ferramentas-dropdown.tsx +++ b/components/navbar/nav-tools.tsx @@ -11,7 +11,7 @@ import { cn } from "@/lib/utils/ui"; const itemClass = "flex w-full items-center gap-2.5 rounded-sm px-2 py-2 text-sm text-foreground hover:bg-accent transition-colors cursor-pointer"; -export function FerramentasDropdownContent() { +export function NavToolsDropdown() { const { privacyMode, toggle } = usePrivacyMode(); const [calcOpen, setCalcOpen] = useState(false); @@ -54,13 +54,11 @@ export function FerramentasDropdownContent() { ); } -type MobileFerramentasItemsProps = { +type MobileToolsProps = { onClose: () => void; }; -export function MobileFerramentasItems({ - onClose, -}: MobileFerramentasItemsProps) { +export function MobileTools({ onClose }: MobileToolsProps) { const { privacyMode, toggle } = usePrivacyMode(); const [calcOpen, setCalcOpen] = useState(false); diff --git a/components/topbar/topbar-user.tsx b/components/navbar/navbar-user.tsx similarity index 97% rename from components/topbar/topbar-user.tsx rename to components/navbar/navbar-user.tsx index 2e253a4..3cb9fe4 100644 --- a/components/topbar/topbar-user.tsx +++ b/components/navbar/navbar-user.tsx @@ -29,7 +29,7 @@ import { Badge } from "../ui/badge"; const itemClass = "flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm transition-colors hover:bg-accent"; -type TopbarUserProps = { +type NavbarUserProps = { user: { id: string; name: string; @@ -39,7 +39,7 @@ type TopbarUserProps = { pagadorAvatarUrl: string | null; }; -export function TopbarUser({ user, pagadorAvatarUrl }: TopbarUserProps) { +export function NavbarUser({ user, pagadorAvatarUrl }: NavbarUserProps) { const router = useRouter(); const [logoutLoading, setLogoutLoading] = useState(false); const [feedbackOpen, setFeedbackOpen] = useState(false); diff --git a/components/topbar/top-nav-menu.tsx b/components/topbar/top-nav-menu.tsx deleted file mode 100644 index ee7ad95..0000000 --- a/components/topbar/top-nav-menu.tsx +++ /dev/null @@ -1,302 +0,0 @@ -"use client"; - -import { - RiArrowLeftRightLine, - RiBankCard2Line, - RiBankLine, - RiCalendarEventLine, - RiDashboardLine, - RiFileChartLine, - RiFundsLine, - RiGroupLine, - RiInboxLine, - RiMenuLine, - RiPriceTag3Line, - RiSparklingLine, - RiTodoLine, -} from "@remixicon/react"; -import { useState } from "react"; -import { Button } from "@/components/ui/button"; -import { - NavigationMenu, - NavigationMenuContent, - NavigationMenuItem, - NavigationMenuList, - NavigationMenuTrigger, -} from "@/components/ui/navigation-menu"; -import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, - SheetTrigger, -} from "@/components/ui/sheet"; -import type { DropdownLinkItem } from "./dropdown-link-list"; -import { DropdownLinkList } from "./dropdown-link-list"; -import { - FerramentasDropdownContent, - MobileFerramentasItems, -} from "./ferramentas-dropdown"; -import { MobileNavLink, MobileSectionLabel } from "./mobile-nav-link"; -import { triggerClass } from "./nav-styles"; -import { SimpleNavLink } from "./simple-nav-link"; - -export function TopNavMenu() { - const [sheetOpen, setSheetOpen] = useState(false); - const close = () => setSheetOpen(false); - - const lancamentosItems: DropdownLinkItem[] = [ - { - href: "/lancamentos", - label: "lançamentos", - icon: , - }, - { - href: "/pre-lancamentos", - label: "pré-lançamentos", - icon: , - }, - { - href: "/calendario", - label: "calendário", - icon: , - }, - ]; - - const financasItems: DropdownLinkItem[] = [ - { - href: "/cartoes", - label: "cartões", - icon: , - }, - { - href: "/contas", - label: "contas", - icon: , - }, - { - href: "/orcamentos", - label: "orçamentos", - icon: , - }, - ]; - - const organizacaoItems: DropdownLinkItem[] = [ - { - href: "/pagadores", - label: "pagadores", - icon: , - }, - { - href: "/categorias", - label: "categorias", - icon: , - }, - { - href: "/anotacoes", - label: "anotações", - icon: , - }, - ]; - - const analiseItems: DropdownLinkItem[] = [ - { - href: "/insights", - label: "insights", - icon: , - }, - { - href: "/relatorios/tendencias", - label: "tendências", - icon: , - }, - { - href: "/relatorios/uso-cartoes", - label: "uso de cartões", - icon: , - }, - ]; - - return ( - <> - {/* Desktop nav */} - - - {/* Mobile hamburger */} - - - - - - - Menu - - - - - - ); -}