sass-imobiliaria/specs/022-ux-audit-imoveis/ux-audit.md

742 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
<select name="sort">
<option value="relevance">Relevância</option>
<option value="price_asc">Menor preço</option>
<option value="price_desc">Maior preço</option>
<option value="area_desc">Maior área</option>
<option value="newest">Mais recente</option>
</select>
```
- 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
<div>
<RangeSlider min={0} max={5_000_000} value={[priceMin, priceMax]} />
<RangeInputs ... />
</div>
```
---
### 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
<div className={`flex flex-col gap-3 transition-opacity ${loading ? 'opacity-50 pointer-events-none' : 'opacity-100'}`}>
{result?.items.map(p => <PropertyRowCard ... />)}
</div>
```
---
### 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
<article className="group bg-panel border border-borderSubtle rounded-2xl overflow-hidden
hover:border-borderStandard transition-all duration-200
flex flex-col sm:flex-row sm:h-[220px]">
{/* Imagem: full-width em mobile, fixed em desktop */}
<div className="relative flex-shrink-0 w-full h-48 sm:w-[280px] sm:h-full lg:w-[340px]">
```
---
### 4.2 Botões dentro de um elemento `<Link>` (problema de semântica) 🔴
**Problema:** O elemento `<Link>` envolve toda a seção de informações do card, incluindo os botões "Comparar" e "Entre em contato". Isso cria elementos interativos aninhados (`<button>` dentro de `<a>`), o que é **inválido no HTML5** e pode causar comportamento inconsistente em diferentes browsers e leitores de tela.
**Solução:** Reestruturar o card para que o `<Link>` seja apenas um elemento de fundo clicável (via `position: absolute`) e os botões fiquem fora do DOM do Link:
```tsx
<article className="relative group ...">
{/* Imagem */}
<div className="...carousel..."></div>
{/* Info section — sem ser um Link diretamente */}
<div className="flex flex-col flex-1 p-5 gap-2">
<Link to={...} className="absolute inset-0" aria-label="Ver detalhes" />
{/* Todo o conteúdo */}
<h3>...</h3>
{/* Botões ficam acima do Link absoluto com z-index */}
<div className="mt-auto flex items-center gap-2 relative z-10">
<button onClick={handleCompareClick}>Comparar</button>
<button onClick={handleContactClick}>Entre em contato</button>
</div>
</div>
</article>
```
---
### 4.3 "Ver detalhes →" como `<span>` não interativo 🟡
**Problema:** O texto `Ver detalhes →` é um `<span>` dentro de um `<Link>`. 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
<span className="ml-auto text-xs font-medium text-accent-violet group-hover:translate-x-0.5 transition-transform inline-flex items-center gap-1">
Ver detalhes <ArrowRightIcon />
</span>
```
---
### 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 && (
<span className="text-[10px] text-textTertiary bg-surface rounded px-1.5 py-0.5 w-fit">
{property.subtype.name}
</span>
)}
```
---
### 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 && (
<span className="bg-amber-500/90 text-white ..."> Destaque</span>
)}
{property.is_new && ( // criado nos últimos 7 dias
<span className="bg-emerald-500/90 text-white ...">Novo</span>
)}
```
---
### 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
<span title="Quartos"><BedIcon /> 3</span>
```
---
## 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
<div className={view === 'grid'
? 'grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-4'
: 'flex flex-col gap-3'}>
```
---
### 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
<p className="text-xs text-textTertiary text-center mt-6">
Exibindo {(page - 1) * perPage + 1}{Math.min(page * perPage, total)} de {total} imóveis
</p>
```
---
### 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<string | null>(null)
// no fetchProperties:
} catch (err) {
setError('Não foi possível carregar os imóveis. Tente novamente.')
} finally {
setLoading(false)
}
// no render:
{error && (
<div className="...">
<p>{error}</p>
<button onClick={() => fetchProperties(filters)}>Tentar novamente</button>
</div>
)}
```
---
## 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) => (
<div
key={property.id}
className="animate-fade-in-up"
style={{ animationDelay: `${i * 40}ms` }}
>
<PropertyRowCard property={property} />
</div>
))}
```
```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 && (
<button
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
className="fixed bottom-24 right-6 z-40 w-10 h-10 rounded-full bg-panel border border-borderStandard shadow-lg ..."
>
</button>
)}
```
---
### 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 — `<button>` dentro de `<a>` é HTML inválido)*
---
### 9.2 Carrossel sem suporte a teclado 🟡
**Problema:** Os botões prev/next do carrossel não têm `tabIndex` e estão ocultos visualmente (`opacity-0`). Um usuário que navega por teclado não consegue navegar pelas fotos.
**Solução:**
```tsx
<button
onClick={prev}
onFocus={() => /* mostrar botões */}
tabIndex={0}
aria-label="Foto anterior"
// Remover opacity-0 do tabIndex focus state
className="... focus:opacity-100"
>
```
---
### 9.3 Dots do carrossel com área clicável pequena 🟡
**Problema:** Os dots têm `w-1.5 h-1.5` (6px). A área mínima recomendada pela WCAG é 24×24px.
**Solução:**
```tsx
// Área de toque maior com padding
<button
className="p-2 -m-2" // área de toque 22px+padding
>
<span className="block w-1.5 h-1.5 rounded-full ..." />
</button>
```
---
### 9.4 `<main>` sem `id` ou `aria-label` 🟢
**Problema:** O `<main>` existe, mas não tem `aria-label` ou `id="main-content"` para skip links.
**Solução:**
```tsx
<main id="main-content" aria-label="Listagem de imóveis" className="pt-14">
```
---
### 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
<aside aria-label="Filtros de busca">
<FilterSidebar ... />
</aside>
```
---
## 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 (768px1023px), 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 && (
<button
onClick={() => loadMore()}
className="w-full mt-6 py-3 rounded-xl border border-borderStandard ..."
>
Carregar mais imóveis ({remaining} restantes)
</button>
)}
```
---
### 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 `<img>` 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 && (
<SlideImage key={i} src={photo.url} alt={...} />
)
))}
```
---
## 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
<button className="rounded-lg px-4 py-2 text-xs font-semibold bg-brand text-white
hover:bg-accentHover transition-colors shadow-sm">
Falar com corretor
</button>
```
---
### 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 && (
<Tooltip content="Máximo de 3 imóveis para comparar. Remova um para adicionar este.">
<button disabled className="opacity-40 cursor-not-allowed ...">Comparar</button>
</Tooltip>
)}
```
---
### 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
<a
href={`https://wa.me/${whatsapp}?text=...`}
target="_blank"
rel="noopener noreferrer"
className="..."
onClick={e => e.stopPropagation()}
>
<WhatsAppIcon />
</a>
```
---
## 13. Roadmap Priorizado
### Sprint 1 — Crítico (impacto imediato na usabilidade)
| # | Item | Esforço | Impacto |
|---|---|---|---|
| 1 | Corrigir botões dentro de `<Link>` (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 (26h) · 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 ("XY 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 "XY 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.*