sass-imobiliaria/.specify/features/013-header-only-nav/plan.md

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.tsxfrontend/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'

NavLink substitui Link nos itens de dropdown para suporte a isActive.

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 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:

{!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.tsxfrontend/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 adminNav no topo do arquivo é removida pois os itens migram para Navbar.tsx.


3. ClientLayout.tsxfrontend/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 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