18 KiB
Implementation Plan: Navegação Unificada no Header (Remoção do Sidebar)
Branch: 013-header-only-nav | Date: 2026-04-14 | Spec: spec.md
Depends On: Nenhuma dependência de feature anterior — alterações puramente de layout/componente frontend.
Summary
Remoção dos sidebars de AdminLayout e ClientLayout, consolidando todos os itens de navegação no Navbar (header fixo) via dropdowns contextuais. O dropdown "Admin ▾" aparece apenas para isAdmin; o dropdown "Minha Conta ▾" aparece apenas para usuários autenticados não-admin. Nenhuma rota nova, nenhuma alteração de backend. Alterações em 3 arquivos frontend: Navbar.tsx, AdminLayout.tsx e ClientLayout.tsx.
Observação sobre ClientLayout + Navbar: ClientLayout.tsx atualmente não importa nem renderiza <Navbar /> (ao contrário de AdminLayout). O App.tsx também não possui um <Navbar /> global. Para que o dropdown "Minha Conta ▾" seja exibido nas rotas /area-do-cliente/*, ClientLayout deve passar a renderizar <Navbar /> como parte desta feature.
Technical Context
Language/Version: TypeScript 5.5 (frontend)
Primary Dependencies: React 18, react-router-dom v6, Tailwind CSS 3.4 (já utilizados — sem novas dependências)
Storage: Nenhuma alteração de banco de dados
Testing: Vite build check (frontend) — sem testes unitários para componentes de layout no projeto atual
Target Platform: SPA (React) servida via Vite proxy
Project Type: Web SPA (React)
Performance Goals: Sem re-renders desnecessários no toggle de dropdown; fechamento por clique externo via useRef + useEffect
Constraints: Sem bibliotecas externas novas; dropdowns implementados com React + Tailwind puro; mobile: itens expandidos inline no menu hamburger (sem toggle aninhado)
Scale/Scope: Refactor de layout puro — 3 arquivos frontend, sem impacto em rotas ou dados
Constitution Check
| Princípio | Status | Observação |
|---|---|---|
| I. Design-First | ✅ PASS | Tokens bg-[#0f1011], border-white/[0.06], bg-white/[0.06], shadow-xl, rounded-xl são idênticos aos já utilizados no projeto conforme DESIGN.md. |
| II. Separation of Concerns | ✅ PASS | Navbar contém lógica de navegação; layouts são thin wrappers sem lógica própria. |
| III. Spec-Driven | ✅ PASS | spec.md aprovado → plan.md (este) → implementação. |
| IV. Data Integrity | ✅ PASS | Nenhuma alteração de dados — feature puramente de UI. |
| V. Security | ✅ PASS | Visibilidade dos dropdowns controlada pelos mesmos guards já existentes (isAdmin, isAuthenticated). Nenhuma rota protegida exposta. |
| VI. Simplicity First | ✅ PASS | Remoção de código (sidebars) > adição. Sem abstração nova, sem nova lib. Alterações cirúrgicas em 3 arquivos. |
POST-DESIGN RE-CHECK: ✅ Adicionar useRef + useEffect ao Navbar e <Navbar /> ao ClientLayout são as únicas adições de infraestrutura — totalmente justificadas pelo requisito de UX.
Architecture Overview
App.tsx (inalterado)
├── /area-do-cliente → ProtectedRoute → ClientLayout → <Outlet />
│ ClientLayout: <> <Navbar /> <main pt-14><Outlet /></main> </>
│ ↑ adicionado nesta feature
│
└── /admin → AdminRoute → AdminLayout → <Outlet />
AdminLayout: <> <Navbar /> <main pt-14><Outlet /></main> </>
(sidebar removido; flex wrapper removido)
Navbar.tsx (ampliado)
├── adminNavItems[] — 7 itens /admin/*
├── clientNavItems[] — 5 itens /area-do-cliente/*
├── adminDropdownOpen (useState)
├── clientDropdownOpen (useState)
├── adminDropdownRef (useRef) + useEffect para fechar no outside click
├── clientDropdownRef (useRef) + useEffect para fechar no outside click
├── Desktop: dropdown "Admin ▾" (isAdmin) / "Minha Conta ▾" (isAuthenticated não-admin)
└── Mobile: itens admin ou cliente expandidos inline no menu hamburger
Frontend Changes
1. Navbar.tsx — frontend/src/components/Navbar.tsx
1.1 Imports adicionais
import { useState, useRef, useEffect } from 'react'
import { Link, NavLink } from 'react-router-dom'
import { useAuth } from '../contexts/AuthContext'
import { ThemeToggle } from './ThemeToggle'
NavLinksubstituiLinknos itens de dropdown para suporte aisActive.
1.2 Arrays de navegação
const adminNavItems = [
{ to: '/admin/properties', label: 'Imóveis' },
{ to: '/admin/clientes', label: 'Clientes' },
{ to: '/admin/boletos', label: 'Boletos' },
{ to: '/admin/visitas', label: 'Visitas' },
{ to: '/admin/favoritos', label: 'Favoritos' },
{ to: '/admin/cidades', label: 'Cidades' },
{ to: '/admin/amenidades', label: 'Amenidades' },
]
const clientNavItems = [
{ to: '/area-do-cliente', label: 'Painel', end: true },
{ to: '/area-do-cliente/favoritos',label: 'Favoritos',end: false },
{ to: '/area-do-cliente/comparar', label: 'Comparar', end: false },
{ to: '/area-do-cliente/visitas', label: 'Visitas', end: false },
{ to: '/area-do-cliente/boletos', label: 'Boletos', end: false },
]
1.3 Estado e refs no componente
const [menuOpen, setMenuOpen] = useState(false)
const [adminDropdownOpen, setAdminDropdownOpen] = useState(false)
const [clientDropdownOpen, setClientDropdownOpen] = useState(false)
const adminDropdownRef = useRef<HTMLDivElement>(null)
const clientDropdownRef = useRef<HTMLDivElement>(null)
// Fechar dropdown ao clicar fora
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (adminDropdownRef.current && !adminDropdownRef.current.contains(e.target as Node)) {
setAdminDropdownOpen(false)
}
if (clientDropdownRef.current && !clientDropdownRef.current.contains(e.target as Node)) {
setClientDropdownOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
1.4 Desktop — Dropdown "Admin ▾"
Substituir o <li> do link "Painel Admin" no navLinks map por:
{isAdmin && (
<li className="relative" ref={adminDropdownRef}>
<button
onClick={() => setAdminDropdownOpen(prev => !prev)}
className="flex items-center gap-1 text-sm text-[#5e6ad2] hover:text-[#7170ff] font-semibold transition-colors"
>
Admin
<span className="text-[10px] opacity-70">▾</span>
</button>
{adminDropdownOpen && (
<div className="absolute top-full right-0 mt-2 w-44 bg-[#111] rounded-xl border border-white/[0.06] shadow-xl py-2 z-50">
{adminNavItems.map(item => (
<NavLink
key={item.to}
to={item.to}
onClick={() => setAdminDropdownOpen(false)}
className={({ isActive }) =>
`block px-4 py-2 text-sm transition-colors ${
isActive
? 'bg-white/[0.06] text-white font-medium'
: 'text-white/60 hover:text-white hover:bg-white/[0.04]'
}`
}
>
{item.label}
</NavLink>
))}
</div>
)}
</li>
)}
1.5 Desktop — Auth section refatorada
Substituir o bloco isAuthenticated && user na auth section (atualmente com "Admin" button + avatar link + "Sair"):
{isAuthenticated && user ? (
<>
{isAdmin ? (
/* Dropdown Admin já está no navLinks — apenas avatar + sair aqui */
<>
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-[#5e6ad2] text-xs font-semibold text-white flex-shrink-0">
{user.name.charAt(0).toUpperCase()}
</span>
<button
onClick={logout}
className="text-sm text-text-secondary hover:text-text-primary transition-colors duration-150 font-medium"
>
Sair
</button>
</>
) : (
/* Dropdown "Minha Conta ▾" para cliente */
<div className="relative" ref={clientDropdownRef}>
<button
onClick={() => setClientDropdownOpen(prev => !prev)}
className="flex items-center gap-2 text-sm text-text-secondary hover:text-text-primary transition-colors duration-150"
>
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-[#5e6ad2] text-xs font-semibold text-white flex-shrink-0">
{user.name.charAt(0).toUpperCase()}
</span>
<span className="max-w-[100px] truncate font-medium">{user.name}</span>
<span className="text-[10px] opacity-70">▾</span>
</button>
{clientDropdownOpen && (
<div className="absolute top-full right-0 mt-2 w-48 bg-[#111] rounded-xl border border-white/[0.06] shadow-xl py-2 z-50">
{clientNavItems.map(item => (
<NavLink
key={item.to}
to={item.to}
end={item.end}
onClick={() => setClientDropdownOpen(false)}
className={({ isActive }) =>
`block px-4 py-2 text-sm transition-colors ${
isActive
? 'bg-white/[0.06] text-white font-medium'
: 'text-white/60 hover:text-white hover:bg-white/[0.04]'
}`
}
>
{item.label}
</NavLink>
))}
<div className="my-1 border-t border-white/[0.06]" />
<button
onClick={() => { setClientDropdownOpen(false); logout() }}
className="block w-full text-left px-4 py-2 text-sm text-white/50 hover:text-white hover:bg-white/[0.04] transition-colors"
>
Sair
</button>
</div>
)}
</div>
)}
</>
) : (
<Link
to="/login"
className="rounded-lg bg-[#5e6ad2] px-4 py-1.5 text-sm font-medium text-white transition hover:bg-[#6872d8]"
>
Entrar
</Link>
)}
Nota: O link "Admin" (botão amarelo) e o link "Painel Admin" em
navLinkssão removidos. O dropdown no botão admin já contém todos os itens.
1.6 Mobile — Itens admin/cliente inline
No mobile menu, substituir o bloco auth existente por:
{!isLoading && (
isAuthenticated && user ? (
<>
{isAdmin
? adminNavItems.map(item => (
<li key={item.to}>
<NavLink
to={item.to}
onClick={() => setMenuOpen(false)}
className={({ isActive }) =>
`block py-2.5 text-sm font-medium transition-colors ${
isActive
? 'text-[#5e6ad2]'
: 'text-[#5e6ad2]/70 hover:text-[#5e6ad2]'
}`
}
>
{item.label}
</NavLink>
</li>
))
: clientNavItems.map(item => (
<li key={item.to}>
<NavLink
to={item.to}
end={item.end}
onClick={() => setMenuOpen(false)}
className={({ isActive }) =>
`block py-2.5 text-sm transition-colors ${
isActive
? 'text-white font-medium'
: 'text-text-secondary hover:text-text-primary'
}`
}
>
{item.label}
</NavLink>
</li>
))
}
<li>
<button
onClick={() => { setMenuOpen(false); logout() }}
className="block py-2.5 text-sm text-text-secondary hover:text-text-primary transition-colors duration-150 font-medium w-full text-left"
>
Sair
</button>
</li>
</>
) : (
<li>
<Link
to="/login"
className="block py-2.5 text-sm font-medium text-[#5e6ad2] hover:text-[#7170ff] transition-colors duration-150"
onClick={() => setMenuOpen(false)}
>
Entrar
</Link>
</li>
)
)}
2. AdminLayout.tsx — frontend/src/layouts/AdminLayout.tsx
Remover <aside> e wrapper <div className="flex ...">. O layout resultante é um thin wrapper:
Antes:
export default function AdminLayout() {
return (
<>
<Navbar />
<div className="flex min-h-screen pt-14 bg-[#08090a]">
<aside className="hidden lg:flex w-56 flex-col border-r border-white/[0.06] bg-panel px-3 py-6">
<nav className="flex flex-1 flex-col gap-0.5">
{adminNav.map(item => (
<NavLink key={item.to} to={item.to} className={...}>
{item.label}
</NavLink>
))}
</nav>
</aside>
<main className="flex-1 min-w-0 overflow-auto">
<Outlet />
</main>
</div>
</>
);
}
Depois:
export default function AdminLayout() {
return (
<>
<Navbar />
<main className="pt-14 min-h-screen bg-[#08090a]">
<Outlet />
</main>
</>
);
}
Imports removidos: NavLink (não utilizado no layout após simplificação). Manter Outlet, Navbar.
A constante
adminNavno topo do arquivo é removida pois os itens migram paraNavbar.tsx.
3. ClientLayout.tsx — frontend/src/layouts/ClientLayout.tsx
Remover <aside>, o mobile nav bar e toda a lógica de navegação local. Adicionar <Navbar /> (atualmente ausente no ClientLayout).
Antes:
import { NavLink, Outlet, useNavigate } from 'react-router-dom';
import { ThemeToggle } from '../components/ThemeToggle';
import { useAuth } from '../contexts/AuthContext';
// ... navItems, adminNavItems arrays ...
export default function ClientLayout() {
const { user, logout } = useAuth();
const navigate = useNavigate();
// ...
return (
<div className="flex min-h-screen bg-[#08090a] pt-14">
<aside ...> {/* sidebar desktop */} </aside>
<main className="flex-1 min-w-0 overflow-auto">
<div className="lg:hidden ..."> {/* mobile nav bar */} </div>
<Outlet />
</main>
</div>
);
}
Depois:
import { Outlet } from 'react-router-dom';
import Navbar from '../components/Navbar';
export default function ClientLayout() {
return (
<>
<Navbar />
<main className="pt-14 min-h-screen bg-[#08090a]">
<Outlet />
</main>
</>
);
}
Imports removidos: NavLink, useNavigate, ThemeToggle, useAuth (toda a lógica de user/logout migra para Navbar).
Imports adicionados: Navbar.
As constantes
navItemseadminNavItemssão removidas — os itens migram paraNavbar.tsx.
Backend Changes
Nenhuma alteração necessária. Feature puramente frontend.
Files Changed Summary
| Arquivo | Tipo | Operação |
|---|---|---|
frontend/src/components/Navbar.tsx |
Frontend | Modificar — adicionar arrays, estados, refs, dropdowns |
frontend/src/layouts/AdminLayout.tsx |
Frontend | Simplificar — remover aside + flex wrapper |
frontend/src/layouts/ClientLayout.tsx |
Frontend | Simplificar — remover aside + mobile nav, adicionar Navbar |
Acceptance Criteria Checklist
AdminLayout.tsx: sem<aside>, semdiv.flex, main ocupa 100% da larguraClientLayout.tsx: sem<aside>, sem mobile nav bar,<Navbar />presenteNavbar.tsx: dropdown "Admin ▾" visível apenas paraisAdminNavbar.tsx: dropdown "Minha Conta ▾" visível apenas paraisAuthenticatednão-admin- Dropdown fecha ao clicar fora (
useRef + useEffect) - Item ativo destacado no dropdown via
NavLink isActive - Mobile: itens admin ou cliente expandidos inline, sem toggle aninhado
- Sem dependências externas novas
vite buildsem erros de TypeScript