feat: add full project - backend, frontend, docker, specs and configs

This commit is contained in:
MatheusAlves96 2026-04-20 23:59:45 -03:00
parent b77c7d5a01
commit e6cb06255b
24489 changed files with 61341 additions and 36 deletions

View file

@ -0,0 +1,742 @@
# 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.*