433 lines
18 KiB
Markdown
433 lines
18 KiB
Markdown
# Implementation Plan: Navegação Unificada no Header (Remoção do Sidebar)
|
|
|
|
**Branch**: `013-header-only-nav` | **Date**: 2026-04-14 | **Spec**: [spec.md](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
|
|
|
|
```tsx
|
|
import { useState, useRef, useEffect } from 'react'
|
|
import { Link, NavLink } from 'react-router-dom'
|
|
import { useAuth } from '../contexts/AuthContext'
|
|
import { ThemeToggle } from './ThemeToggle'
|
|
```
|
|
|
|
> `NavLink` substitui `Link` nos itens de dropdown para suporte a `isActive`.
|
|
|
|
#### 1.2 Arrays de navegação
|
|
|
|
```tsx
|
|
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
|
|
|
|
```tsx
|
|
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:
|
|
|
|
```tsx
|
|
{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"):
|
|
|
|
```tsx
|
|
{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 `navLinks` sã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:
|
|
|
|
```tsx
|
|
{!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:**
|
|
```tsx
|
|
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:**
|
|
```tsx
|
|
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 `adminNav` no topo do arquivo é removida pois os itens migram para `Navbar.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:**
|
|
```tsx
|
|
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:**
|
|
```tsx
|
|
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 `navItems` e `adminNavItems` são removidas — os itens migram para `Navbar.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>`, sem `div.flex`, main ocupa 100% da largura
|
|
- [ ] `ClientLayout.tsx`: sem `<aside>`, sem mobile nav bar, `<Navbar />` presente
|
|
- [ ] `Navbar.tsx`: dropdown "Admin ▾" visível apenas para `isAdmin`
|
|
- [ ] `Navbar.tsx`: dropdown "Minha Conta ▾" visível apenas para `isAuthenticated` nã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 build` sem erros de TypeScript
|