# UX/UI Audit — Página `/imoveis` > Análise realizada em 18/04/2026. Baseada no código-fonte atual da `PropertiesPage`, `FilterSidebar`, `PropertyRowCard`, `ContactModal` e componentes relacionados. --- ## Índice 1. [Diagnóstico Geral](#1-diagnóstico-geral) 2. [Arquitetura de Informação](#2-arquitetura-de-informação) 3. [Filtros e Busca](#3-filtros-e-busca) 4. [Cards de Imóvel](#4-cards-de-imóvel) 5. [Layout e Visualização](#5-layout-e-visualização) 6. [Paginação](#6-paginação) 7. [Estado Vazio e Erros](#7-estado-vazio-e-erros) 8. [Micro-interações e Animações](#8-micro-interações-e-animações) 9. [Acessibilidade (A11y)](#9-acessibilidade-a11y) 10. [Mobile Experience](#10-mobile-experience) 11. [Performance Percebida](#11-performance-percebida) 12. [Fluxo de Conversão](#12-fluxo-de-conversão) 13. [Roadmap Priorizado](#13-roadmap-priorizado) --- ## 1. Diagnóstico Geral ### Pontos positivos atuais - Filtros sincronizados com a URL — links são compartilháveis e o histórico do browser funciona corretamente. - Skeleton loading implementado — evita CLS (Cumulative Layout Shift). - Carrossel de fotos com lazy load por slide individual. - Sidebar sticky no desktop, drawer no mobile. - Favoritos e comparação com contexto global persistente. - Badge de contagem de filtros ativos no botão mobile. ### Problemas críticos identificados | Severidade | Quantidade | Descrição resumida | |---|---|---| | 🔴 Alta | 5 | Impactam diretamente conversão ou usabilidade fundamental | | 🟡 Média | 9 | Degradam a experiência sem bloquear o uso | | 🟢 Baixa | 8 | Refinamentos e polish | --- ## 2. Arquitetura de Informação ### 2.1 Ausência de campo de busca textual 🔴 **Problema:** Não existe um input de busca livre (por endereço, bairro, título ou código do imóvel). O usuário só consegue filtrar via dropdowns/chips. Para alguém que já sabe o endereço ou código do imóvel, o fluxo é completamente ineficiente. **Referências:** Todos os grandes portais imobiliários (Zap, VivaReal, OLX) colocam a busca textual como primeiro ponto de entrada. **Solução recomendada:** ``` ┌─────────────────────────────────────────────────────┐ │ 🔍 Buscar por endereço, bairro ou código... │ └─────────────────────────────────────────────────────┘ ``` - Barra de busca proeminente no topo da área de resultados (não na sidebar). - Debounce de 400ms para evitar requests excessivos. - Parâmetro `q` na URL: `/imoveis?q=Barra+Funda`. - Busca no backend via `ILIKE` em `title`, `address`, `code`, `neighborhood.name`. --- ### 2.2 Ausência de ordenação 🔴 **Problema:** Não há opção para ordenar os resultados. O usuário não controla se quer ver por menor preço, maior área, mais recente ou destaque. **Solução recomendada:** ```tsx // Dropdown de ordenação ao lado do contador de resultados ``` - Parâmetro `sort` na URL. - Persistir a preferência de ordenação na URL (já funciona com o sistema atual de `filtersToParams`). --- ### 2.3 Falta de chips/tags de filtros ativos 🟡 **Problema:** Quando o usuário aplica filtros no desktop, não há feedback visual na área de resultados mostrando *quais* filtros estão ativos. O contador "87 imóveis encontrados" não revela *por quê* esse número é aquele. **Solução recomendada:** ``` ┌─ Filtros ativos ──────────────────────────────────────┐ │ [Aluguel ×] [São Paulo ×] [2+ quartos ×] [Limpar tudo] │ └───────────────────────────────────────────────────────┘ ``` - Chips removíveis logo abaixo do header, acima do primeiro card. - Cada chip tem `×` para remover individualmente. - Botão "Limpar tudo" só aparece quando há ≥ 2 chips. --- ### 2.4 Sem breadcrumbs 🟢 **Problema:** Não há indicação de onde o usuário está na hierarquia do site. Especialmente útil quando vem de uma busca filtrada. **Solução:** Breadcrumb minimalista no header: `Início > Imóveis > Apartamentos em São Paulo`. --- ## 3. Filtros e Busca ### 3.1 Sidebar muito estreita para inputs de range 🟡 **Problema:** A sidebar tem `w-56` (224px). Os `RangeInputs` de preço e área ficam comprimidos, forçando o usuário a digitar em campos muito pequenos sem feedback visual do range selecionado. **Solução recomendada:** - Aumentar para `w-64` (256px) ou `w-72` (288px) no desktop. - Adicionar slider visual (range input duplo) acima dos inputs numéricos para preço — visualmente mais intuitivo e mais rápido que digitar valores. ```tsx // Price range com slider + inputs
``` --- ### 3.2 Drawer mobile abre à direita 🟡 **Problema:** O drawer de filtros mobile abre pela direita (`absolute right-0`). O padrão de mercado (Google, Apple HIG) é que filtros e navegação secundária abram pela **esquerda**. Abrir pela direita quebra a expectativa de usuários que associam o lado direito a ações e notificações. **Solução:** Mudar para `left-0` com largura de `85vw` máx `360px`. O botão de filtro pode ficar onde está. --- ### 3.3 Nenhum feedback de "aplicando filtros" 🟡 **Problema:** Ao mudar qualquer filtro no sidebar desktop, a lista já recarrega (debounced pelo `useEffect`). Porém o usuário não tem feedback imediato de que *algo está acontecendo* — o skeleton só aparece depois do delay de rede, criando uma janela de latência percebida. **Solução:** - Mostrar um indicador sutil (spinner ou barra de progresso no topo da lista) imediatamente ao alterar filtros, antes do resultado da API. - Adicionar transição `opacity: 0.5` na lista enquanto `loading === true`, mantendo os cards anteriores visíveis em vez de sumindo e mostrando skeletons. ```tsx // Ao invés de mostrar skeleton completo, apenas escurecer
{result?.items.map(p => )}
``` --- ### 3.4 Checkbox "incluir condomínio" ambíguo 🟢 **Problema:** O checkbox "incluir condomínio" no filtro de preço não é óbvio. Um usuário leigo pode não entender se isso soma o valor do condomínio ao preço de aluguel para filtrar, ou se está filtrando imóveis que *tenham* condomínio. **Solução:** - Tooltip (?) com explicação: *"Quando ativo, o filtro de preço inclui a taxa de condomínio no total considerado."* - Ou reescrever o label para: `Preço total (com condomínio)`. --- ### 3.5 Comodidades sempre colapsadas 🟢 **Problema:** Os grupos de comodidades (Lazer, Segurança, etc.) têm `defaultOpen = false`. Em um sidebar já scrollável, isso exige que o usuário descubra que essas seções existem e clique para expandir. **Solução:** Manter colapsado por padrão, mas mostrar os chips das comodidades selecionadas *mesmo colapsado*, para que o usuário veja que há filtros ativos ali. --- ## 4. Cards de Imóvel ### 4.1 Altura fixa quebra o layout em telas menores 🔴 **Problema:** `h-[220px]` fixo no card e `w-[340px]` fixo na imagem. Em viewports entre 768px e 1024px (tablets), o card fica comprimido mas mantém a altura rígida, fazendo o texto ser truncado prematuramente. **Impacto:** Usuários em tablets iPad (768px) ficam no breakpoint errado — recebem o layout desktop comprimido, não o mobile drawer. **Solução recomendada:** ```tsx // Card com altura adaptável
{/* Imagem: full-width em mobile, fixed em desktop */}
``` --- ### 4.2 Botões dentro de um elemento `` (problema de semântica) 🔴 **Problema:** O elemento `` envolve toda a seção de informações do card, incluindo os botões "Comparar" e "Entre em contato". Isso cria elementos interativos aninhados (`
``` --- ### 4.3 "Ver detalhes →" como `` não interativo 🟡 **Problema:** O texto `Ver detalhes →` é um `` dentro de um ``. Visualmente parece um link separado, mas não tem estados de hover/focus próprios. Usuários podem tentar clicar especificamente nele achando que é um CTA distinto. **Solução:** Transformar em um botão visual real com seta animada no hover: ```tsx Ver detalhes ``` --- ### 4.4 Tipo de imóvel ausente no card 🟡 **Problema:** O card não exibe o tipo/subtipo do imóvel (Apartamento, Casa, Sala Comercial, etc.). O usuário precisa entrar na página de detalhes para descobrir o tipo, mesmo que já esteja filtrando por tipo. **Solução:** Adicionar o subtipo como um chip pequeno abaixo do título: ```tsx {property.subtype && ( {property.subtype.name} )} ``` --- ### 4.5 Carrossel inacessível em mobile (hover-only) 🔴 **Problema:** Os botões prev/next do carrossel têm `opacity-0 group-hover:opacity-100`. Em dispositivos touch, não existe o estado `:hover`, tornando os botões de navegação **completamente invisíveis e inacessíveis** em mobile. **Solução:** ```tsx // Tornar visível em mobile, apenas com hover no desktop className="... opacity-100 sm:opacity-0 sm:group-hover:opacity-100 ..." ``` Ou usar swipe gesture nativo em mobile (touchstart/touchend). --- ### 4.6 Ausência de badge "Novo" e "Destaque" 🟢 **Problema:** Não há diferenciação visual entre imóveis recém adicionados, imóveis em destaque ou imóveis com preço reduzido. Isso reduz a urgência percebida e o valor editorial da listagem. **Solução:** ```tsx {property.is_featured && ( ⭐ Destaque )} {property.is_new && ( // criado nos últimos 7 dias Novo )} ``` --- ### 4.7 Abreviações confusas nas stats 🟢 **Problema:** `qts` (quartos) e `ban` (banheiros) são abreviações pouco intuitivas, especialmente para usuários menos familiarizados. `m²` não tem o label "Área". **Solução:** ``` 3 quartos · 2 banheiros · 85 m² · 1 vaga ``` Em espaços comprimidos, usar tooltips: ```tsx 3 ``` --- ## 5. Layout e Visualização ### 5.1 Único modo de visualização (lista) 🟡 **Problema:** A página só oferece visualização em lista horizontal. Muitos usuários preferem visualização em **grade** (especialmente para imóveis com fotos bonitas) pois permite comparar mais imóveis visualmente de uma vez. **Solução:** Toggle de visualização no header: ``` [≡ Lista] [⊞ Grade] ``` - Lista: layout atual `PropertyRowCard` (horizontal, 1 coluna). - Grade: cards verticais `PropertyGridCard` (2-3 colunas, foto em cima), com menos detalhes mas foto maior. - Persistir preferência em `localStorage`. ```tsx // Grade: 2-3 colunas responsivas
``` --- ### 5.2 Per page fixo em 16 🟢 **Problema:** O `per_page: 16` é hardcoded em dois lugares (`handleFiltersChange` e `handleClear`). O usuário não pode escolher ver mais ou menos resultados por página. **Solução:** Seletor discreto no header: `Exibir: [16] [32] [48]`. --- ## 6. Paginação ### 6.1 Sem indicador de posição 🟡 **Problema:** A paginação mostra apenas os números de página mas não informa *quantos resultados* estão sendo exibidos em relação ao total. "Página 3 de 12" não comunica que estamos vendo "imóveis 33-48 de 185". **Solução:** ```tsx // Acima da paginação ou integrado ao header

Exibindo {(page - 1) * perPage + 1}–{Math.min(page * perPage, total)} de {total} imóveis

``` --- ### 6.2 Scroll to top abrupto 🟡 **Problema:** `window.scrollTo({ top: 0, behavior: 'smooth' })` faz o scroll mas não há indicação visual de que os resultados mudaram. O usuário pode não perceber que está vendo novos resultados. **Solução:** - Adicionar uma transição sutil de `opacity` nos cards ao trocar de página. - Ou scroll para o topo do container de resultados, não da janela inteira (para não esconder o sidebar). ```tsx // Scroll para o topo da área de resultados gridRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) ``` --- ### 6.3 Paginação duplicada 🟢 **Melhoria:** Adicionar paginação também no **topo** da lista de resultados (acima do primeiro card). Útil para quando o usuário quer mudar de página sem scrollar até o fim. --- ## 7. Estado Vazio e Erros ### 7.1 Estado vazio sem sugestões 🟡 **Problema atual:** ``` Nenhum imóvel encontrado com esses filtros. [Limpar filtros] ``` Esse estado desperdiça uma oportunidade de reter o usuário e ajudá-lo a encontrar algo relevante. **Solução — Empty State rico:** ``` 😕 Nenhum imóvel encontrado Tente estas sugestões: • [Remover filtro de bairro] → 12 imóveis disponíveis • [Ampliar faixa de preço] → 8 imóveis disponíveis • [Mudar para 1+ quartos] → 5 imóveis disponíveis [Ou limpar todos os filtros →] ``` Para calcular as sugestões, fazer requests paralelos com filtros relaxados. --- ### 7.2 Sem tratamento de erro de rede 🔴 **Problema:** O `fetchProperties` usa `try/finally` mas não guarda o erro. Se a API falhar, a lista simplesmente permanece em estado anterior ou vazia, sem nenhuma mensagem ao usuário. **Solução:** ```tsx const [error, setError] = useState(null) // no fetchProperties: } catch (err) { setError('Não foi possível carregar os imóveis. Tente novamente.') } finally { setLoading(false) } // no render: {error && (

{error}

)} ``` --- ## 8. Micro-interações e Animações ### 8.1 Sem animação de entrada nos cards 🟢 **Problema:** Os cards aparecem abruptamente após o loading. Uma animação sutil de entrada melhora a percepção de qualidade. **Solução com Tailwind + delay staggered:** ```tsx {result.items.map((property, i) => (
))} ``` ```css /* Em index.css */ @keyframes fade-in-up { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } .animate-fade-in-up { animation: fade-in-up 0.25s ease both; } ``` --- ### 8.2 Sem botão "Voltar ao topo" 🟢 **Problema:** Em listas longas (16 cards), o usuário precisa scrollar muito para voltar ao topo e ajustar filtros no sidebar desktop ou clicar no botão de filtros mobile. **Solução:** Botão flutuante que aparece após scrollar 400px: ```tsx {scrollY > 400 && ( )} ``` --- ### 8.3 Barra de comparação conflita com ComparisonBar 🟢 **Problema:** A `ComparisonBar` fica fixa no `bottom-0`. Quando ativa, ela sobrepõe o rodapé e pode encobrir o botão "Voltar ao topo". Sem margem dinâmica no conteúdo principal, cards podem ficar atrás da barra. **Solução:** Quando `ComparisonBar` está visível, adicionar `pb-20` no container principal. --- ## 9. Acessibilidade (A11y) ### 9.1 Elementos interativos aninhados 🔴 *(Ver seção 4.2 — `
``` --- ### 9.4 `
` sem `id` ou `aria-label` 🟢 **Problema:** O `
` existe, mas não tem `aria-label` ou `id="main-content"` para skip links. **Solução:** ```tsx
``` --- ### 9.5 Filtros sidebar sem `role="search"` ou `aria-label` 🟢 **Problema:** O sidebar de filtros não tem semântica ARIA adequada. **Solução:** ```tsx ``` --- ## 10. Mobile Experience ### 10.1 Layout horizontal comprimido em tablet 🔴 *(Ver seção 4.1 — altura fixa do card)* O breakpoint `lg:hidden` para o sidebar significa que em tablets (768px–1023px), o usuário recebe o layout desktop mas com espaço insuficiente. **Solução:** Mudar o breakpoint do sidebar para `md:block` ou `xl:block` e ajustar o layout do card para ser vertical até `xl`. --- ### 10.2 Sem infinite scroll como alternativa 🟢 **Problema:** A paginação tradicional obriga o usuário a clicar e esperar. Em mobile, infinite scroll ou "Carregar mais" é mais natural. **Solução opcional:** Botão "Ver mais imóveis" no final da lista (append, não replace) como alternativa à paginação: ```tsx {result.page < result.pages && ( )} ``` --- ### 10.3 Tipografia pequena demais em mobile 🟡 **Problema:** `text-xs` (12px) em múltiplos lugares (stats do card, labels de filtro). A WCAG recomenda mínimo 16px para corpo de texto em mobile. **Elementos afetados:** - Stats do card (`text-xs text-textSecondary`) - Label do botão "Entre em contato" (`text-xs font-semibold`) - Paginação (`text-xs`) - Seção titles da sidebar (`text-xs font-medium uppercase`) **Solução:** Escalar para `text-sm` (14px) em mobile, mantendo `text-xs` apenas em elementos secundários decorativos. --- ## 11. Performance Percebida ### 11.1 5 requests paralelos antes de renderizar qualquer coisa 🟡 **Problema:** O `Promise.all` na montagem carrega tipos, comodidades, cidades, bairros e imobiliárias antes de mostrar o sidebar. Enquanto isso, a sidebar fica vazia. **Solução:** - Mostrar skeleton da sidebar enquanto os dados de catálogo carregam. - Priorizar o request de `getProperties` (o mais importante) e deixar o catálogo carregar em segundo plano. ```tsx // Separar o loading do catálogo do loading dos imóveis const [catalogLoading, setCatalogLoading] = useState(true) // Catalog carrega em background, não bloqueia os imóveis useEffect(() => { Promise.all([...]).then(([...]) => { // set states setCatalogLoading(false) }) }, []) ``` --- ### 11.2 Todas as fotos do carrossel são renderizadas no DOM 🟡 **Problema:** O `PhotoCarousel` renderiza *todos* os slides no DOM desde o início, apenas mudando a `opacity`. Um imóvel com 10 fotos renderiza 10 `` tags, mesmo que o usuário veja apenas 1. **Solução:** Renderizar apenas o slide atual e os adjacentes (virtualização simples): ```tsx // Renderizar apenas slide atual ± 1 {slides.map((photo, i) => ( Math.abs(i - current) <= 1 && ( ) ))} ``` --- ## 12. Fluxo de Conversão ### 12.1 "Entre em contato" visualmente fraco 🟡 **Problema:** O botão verde `bg-emerald-500` é a única ação de conversão no card, mas divide espaço igualmente com "Comparar" (border simples). O olho do usuário não é guiado para o CTA principal. **Hierarquia visual atual:** `[Comparar]` ≈ `[Entre em contato]` `Ver detalhes →` **Solução — hierarquia clara:** ``` [Ver detalhes] → ação primária (fundo brand) [Entre em contato] → ação secundária (outline brand) [Comparar] → ação terciária (ghost/minimal) ``` Ou: tornar "Entre em contato" o único botão com cor de fundo, ampliado: ```tsx ``` --- ### 12.2 Comparação sem limite visual claro 🟢 **Problema:** Quando o usuário tenta adicionar um 4º imóvel à comparação, o botão simplesmente não funciona (por lógica no contexto), sem feedback visual do motivo. **Solução:** ```tsx // Quando comparação está cheia e imóvel não está na lista {!inComparison && comparisonFull && ( )} ``` --- ### 12.3 WhatsApp como CTA não aparece no card 🟢 **Problema:** O número de WhatsApp existe no sistema e o `ContactModal` o utiliza, mas o card não oferece acesso direto. Muitos usuários preferem o WhatsApp a preencher um formulário. **Solução (opcional):** Ícone de WhatsApp como botão terciário no card: ```tsx e.stopPropagation()} > ``` --- ## 13. Roadmap Priorizado ### Sprint 1 — Crítico (impacto imediato na usabilidade) | # | Item | Esforço | Impacto | |---|---|---|---| | 1 | Corrigir botões dentro de `` (semântica HTML) | P | 🔴 | | 2 | Carrossel visível em mobile (remover opacity-0 em touch) | P | 🔴 | | 3 | Tratamento de erro de rede no fetch de imóveis | P | 🔴 | | 4 | Layout do card responsivo (sem altura fixa) | M | 🔴 | | 5 | Campo de busca textual | G | 🔴 | > P = Pequeno (< 2h) · M = Médio (2–6h) · G = Grande (> 6h) --- ### Sprint 2 — Alto valor (conversão e descoberta) | # | Item | Esforço | Impacto | |---|---|---|---| | 6 | Ordenação de resultados (preço, área, data) | M | 🟡 | | 7 | Chips de filtros ativos com remoção individual | M | 🟡 | | 8 | Toggle lista/grade com `PropertyGridCard` | G | 🟡 | | 9 | Estado vazio com sugestões de filtros relaxados | M | 🟡 | | 10 | Hierarquia de CTAs no card (primário/secundário/terciário) | P | 🟡 | --- ### Sprint 3 — Refinamentos (qualidade percebida) | # | Item | Esforço | Impacto | |---|---|---|---| | 11 | Animação de entrada dos cards (stagger) | P | 🟢 | | 12 | Indicador de posição na paginação ("X–Y de Z imóveis") | P | 🟢 | | 13 | Botão "Voltar ao topo" flutuante | P | 🟢 | | 14 | Badge "Novo" e "Destaque" no card | P | 🟢 | | 15 | Suporte a teclado no carrossel | M | 🟢 | | 16 | Tipo de imóvel no card (subtipo) | P | 🟢 | | 17 | Virtualização de slides do carrossel | M | 🟢 | | 18 | Skeleton do sidebar enquanto catálogo carrega | P | 🟢 | | 19 | Slider visual para range de preço | G | 🟢 | | 20 | Paginação no topo da lista | P | 🟢 | --- ## Referências e Benchmarks | Portal | Funcionalidade de referência | |---|---| | **VivaReal** | Chips de filtros ativos, toggle lista/mapa/grade, busca textual no topo | | **Zap Imóveis** | Ordenação proeminente, cards com subtipo e badges, infinite scroll mobile | | **Airbnb** | Slider de preço com histograma de distribuição, filtros modais ricos | | **Booking.com** | Estado vazio com sugestões de relaxamento de filtros | | **Rightmove (UK)** | Paginação com "X–Y de Z", salvar busca, alertas por email | --- *Documento gerado para uso interno do projeto saas_imobiliaria. Revisão recomendada após implementação de cada sprint.*