sass-imobiliaria/specs/023-ux-melhorias-imoveis/spec.md

254 lines
21 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.

# Feature Specification: Melhorias UX/UI — Listagem de Imóveis
**Feature Branch**: `023-ux-melhorias-imoveis`
**Created**: 2026-04-18
**Status**: Draft
**Fonte**: Auditoria UX/UI `specs/022-ux-audit-imoveis/ux-audit.md`
---
## Contexto
A página `/imoveis` (listagem de imóveis) apresenta 22 problemas identificados em auditoria UX/UI realizada em 18/04/2026. Este spec cobre as 20 melhorias priorizadas em 3 sprints, abrangendo correções críticas de usabilidade, funcionalidades de alto valor para conversão e refinamentos de qualidade percebida.
---
## User Scenarios & Testing
### User Story 1 — Correções Críticas de Usabilidade (Priority: P1)
Um visitante acessa a listagem de imóveis em qualquer dispositivo (desktop, tablet ou mobile) e espera poder navegar, visualizar fotos e receber feedback adequado em caso de falha de rede — sem encontrar elementos quebrados ou comportamento inconsistente.
**Why this priority**: Problemas de semântica HTML inválida, carrossel inacessível em mobile, ausência de tratamento de erro e layout quebrado em tablets impactam diretamente todos os usuários e podem bloquear conversões.
**Independent Test**: Abrir a página `/imoveis` em um dispositivo mobile, navegar pelas fotos de um card tocando nos botões prev/next, simular falha de rede e verificar se mensagem de erro aparece.
**Acceptance Scenarios**:
1. **Given** um card de imóvel com múltiplas fotos, **When** o usuário acessa em dispositivo mobile (touch), **Then** os botões prev/next do carrossel são visíveis e funcionais sem necessidade de hover.
2. **Given** que a API de imóveis retorna erro de rede, **When** a página tenta carregar os imóveis, **Then** uma mensagem de erro é exibida com botão "Tentar novamente".
3. **Given** um card de imóvel, **When** o usuário inspeciona o DOM, **Then** nenhum elemento `<button>` está aninhado dentro de um elemento `<a>`, e todos os botões e links são elementos independentes.
4. **Given** um viewport de 7681023px (tablet), **When** o usuário visualiza os cards de imóveis, **Then** o layout do card se adapta sem altura fixa que trunque o conteúdo.
5. **Given** que múltiplos filtros estão ativos, **When** o usuário aplica novos filtros no desktop, **Then** um indicador visual sutil (opacidade reduzida nos cards) aparece imediatamente, antes do resultado da API chegar.
---
### User Story 2 — Campo de Busca Textual (Priority: P1)
Um visitante que já sabe o endereço, bairro ou código do imóvel que procura quer digitar o termo diretamente na página de listagem e ver resultados filtrados instantaneamente, sem precisar navegar por dropdowns.
**Why this priority**: A ausência de busca textual é o problema de arquitetura de informação mais crítico — todos os grandes portais imobiliários oferecem busca textual como ponto de entrada principal. Bloqueia completamente o fluxo de usuários que chegam com uma intenção específica.
**Independent Test**: Digitar "Barra Funda" no campo de busca e verificar que a URL muda para `/imoveis?q=Barra+Funda` e os resultados são filtrados.
**Acceptance Scenarios**:
1. **Given** a página `/imoveis`, **When** o usuário carrega a página, **Then** um campo de busca textual está visível no topo da área de resultados, com placeholder "Buscar por endereço, bairro ou código...".
2. **Given** o campo de busca preenchido com "Jardins", **When** o usuário para de digitar por 400ms, **Then** a listagem é atualizada com imóveis cujo título, endereço, bairro ou código contenha "Jardins".
3. **Given** uma busca ativa com `q=jardins`, **When** o usuário compartilha o link, **Then** o destinatário vê os mesmos resultados filtrados ao acessar a URL.
4. **Given** o campo de busca preenchido, **When** o usuário clica no botão `×` ou apaga o texto, **Then** o filtro de busca é removido e os resultados voltam ao estado sem filtro de texto.
5. **Given** uma busca que não retorna resultados, **When** a listagem é atualizada, **Then** o estado vazio é exibido com sugestões de termos alternativos.
---
### User Story 3 — Ordenação de Resultados (Priority: P2)
Um visitante quer controlar a ordem de exibição dos imóveis para ver primeiro os mais baratos, os maiores, os mais recentes ou os em destaque, sem precisar percorrer toda a listagem manualmente.
**Why this priority**: A ausência de ordenação obriga o usuário a depender inteiramente da ordem padrão do backend, removendo o controle que usuários esperam em qualquer catálogo de produtos.
**Independent Test**: Selecionar "Menor preço" no dropdown de ordenação e verificar que os cards se reorganizam por ordem crescente de preço.
**Acceptance Scenarios**:
1. **Given** a listagem de imóveis, **When** o usuário vê o header de resultados, **Then** um seletor de ordenação está visível ao lado do contador de resultados.
2. **Given** o seletor de ordenação, **When** o usuário seleciona "Menor preço", **Then** os imóveis são reordenados por preço crescente e o parâmetro `sort=price_asc` aparece na URL.
3. **Given** ordenação "Mais recente" ativa, **When** o usuário compartilha o link, **Then** o destinatário vê os resultados na mesma ordem.
4. **Given** uma ordenação ativa, **When** o usuário troca de página, **Then** a ordenação é mantida na nova página.
**Opções de ordenação disponíveis**:
- Relevância (padrão)
- Menor preço (`price_asc`)
- Maior preço (`price_desc`)
- Maior área (`area_desc`)
- Mais recente (`newest`)
---
### User Story 4 — Chips de Filtros Ativos (Priority: P2)
Um visitante com múltiplos filtros aplicados quer ver imediatamente quais filtros estão ativos e poder remover individualmente cada um deles sem precisar abrir o sidebar.
**Why this priority**: A ausência de chips de filtros ativos cria opacidade no estado atual da busca. O usuário não consegue entender por que o número de resultados é pequeno sem abrir o sidebar.
**Independent Test**: Aplicar filtros de tipo "Aluguel", cidade "São Paulo" e "2+ quartos"; verificar que chips aparecem acima dos resultados com botão de remoção individual em cada um.
**Acceptance Scenarios**:
1. **Given** pelo menos um filtro ativo, **When** o usuário vê a área de resultados, **Then** chips dos filtros ativos aparecem logo abaixo do campo de busca, acima do primeiro card.
2. **Given** chips de filtros visíveis, **When** o usuário clica no `×` de um chip específico, **Then** aquele filtro é removido individualmente e a listagem é atualizada.
3. **Given** dois ou mais filtros ativos, **When** os chips são exibidos, **Then** um botão "Limpar tudo" aparece ao lado dos chips.
4. **Given** nenhum filtro ativo, **When** a listagem é exibida, **Then** a área de chips não é renderizada.
---
### User Story 5 — Toggle de Visualização Lista/Grade (Priority: P2)
Um visitante que prefere comparar imóveis visualmente quer alternar entre visualização em lista (detalhada) e grade (fotos maiores, mais imóveis visíveis), com a preferência salva para próximas visitas.
**Why this priority**: A visualização em grade é especialmente valiosa para imóveis com fotos bonitas e para usuários em fase de descoberta. Aumenta o engajamento e o número de imóveis visualizados por sessão.
**Independent Test**: Clicar no botão de grade, verificar que os cards mudam para layout vertical com 2-3 colunas, e recarregar a página verificando que a preferência foi mantida.
**Acceptance Scenarios**:
1. **Given** a página `/imoveis`, **When** o usuário vê o header de resultados, **Then** dois botões de toggle de visualização estão visíveis: "Lista" (ativo por padrão) e "Grade".
2. **Given** o toggle de Grade selecionado, **When** a listagem é renderizada, **Then** os imóveis aparecem em grade de 1 coluna (mobile), 2 colunas (tablet) e 3 colunas (desktop), com foto em destaque acima das informações.
3. **Given** que o usuário selecionou visualização em grade, **When** ele recarrega a página, **Then** a listagem abre em modo grade (preferência salva localmente).
4. **Given** visualização em grade, **When** o usuário clica em um card, **Then** é redirecionado para a página de detalhes do imóvel normalmente.
---
### User Story 6 — Estado Vazio com Sugestões (Priority: P2)
Um visitante cuja combinação de filtros não retorna resultados recebe sugestões acionáveis de como relaxar os filtros para encontrar imóveis, em vez de uma mensagem genérica de "nenhum resultado".
**Why this priority**: O estado vazio atual desperdiça a oportunidade de reter o usuário. Sugestões de relaxamento de filtros reduzem abandonos e aumentam a chance de conversão.
**Independent Test**: Aplicar filtros impossíveis (ex.: 10+ quartos em bairro específico) e verificar que sugestões com contagem de resultados são exibidas.
**Acceptance Scenarios**:
1. **Given** filtros que retornam zero imóveis, **When** a listagem é atualizada, **Then** o estado vazio exibe sugestões específicas de filtros que podem ser relaxados, cada uma com a quantidade de imóveis que seria encontrada.
2. **Given** o estado vazio com sugestões, **When** o usuário clica em uma sugestão (ex.: "Ampliar faixa de preço"), **Then** o filtro correspondente é ajustado automaticamente e a listagem atualiza com os resultados sugeridos.
3. **Given** o estado vazio, **When** o usuário vê a tela, **Then** um botão "Limpar todos os filtros" é exibido como opção de escape.
---
### User Story 7 — Hierarquia Visual de CTAs no Card (Priority: P2)
Um visitante que vê a listagem de imóveis é guiado visualmente para a ação mais importante do card (ver detalhes), com ações secundárias (contato) e terciárias (comparar) em destaque progressivamente menor.
**Why this priority**: A hierarquia visual de CTAs impacta diretamente a taxa de conversão. Botões com peso visual equivalente não guiam o olho do usuário para a ação desejada.
**Independent Test**: Visualizar a listagem e verificar que "Ver detalhes" tem destaque primário (fundo colorido), "Entre em contato" tem destaque secundário (borda) e "Comparar" tem destaque terciário (ghost/minimal).
**Acceptance Scenarios**:
1. **Given** um card de imóvel, **When** o usuário vê os botões de ação, **Then** "Ver detalhes" tem estilo primário (fundo da cor da marca), "Entre em contato" tem estilo secundário (outline) e "Comparar" tem estilo terciário (ghost).
2. **Given** visualização em mobile, **When** o card é exibido, **Then** o botão "Ver detalhes" continua sendo o CTA mais destacado visualmente.
---
### User Story 8 — Refinamentos de Qualidade (Priority: P3)
Um visitante experimenta a listagem com animações suaves de entrada, indicadores claros de posição na paginação, botão de retorno ao topo, badges de status nos imóveis e navegação por teclado no carrossel.
**Why this priority**: Esses refinamentos aumentam a percepção de qualidade e polimento do produto, mas não bloqueiam nenhum fluxo de uso.
**Independent Test**: Navegar para a página 2, verificar o indicador "Exibindo XY de Z imóveis"; pressionar Tab para focar no carrossel e usar setas do teclado para navegar pelas fotos.
**Acceptance Scenarios**:
1. **Given** uma nova página de resultados carregada, **When** os cards aparecem, **Then** cada card entra com animação sutil de fade-in-up com atraso crescente (stagger de ~40ms por card).
2. **Given** a paginação, **When** o usuário vê o rodapé da listagem, **Then** o texto "Exibindo XY de Z imóveis" está visível acima ou integrado à paginação.
3. **Given** scroll de mais de 400px na página, **When** o botão "Voltar ao topo" flutuante aparece, **Then** clicar nele rola suavemente para o topo da página.
4. **Given** um imóvel marcado como destaque (`is_featured = true`), **When** o card é exibido, **Then** um badge "Destaque" é visível na foto do imóvel.
5. **Given** um imóvel criado nos últimos 7 dias, **When** o card é exibido, **Then** um badge "Novo" é visível na foto do imóvel.
6. **Given** o carrossel de fotos de um card, **When** o usuário navega via teclado (Tab para focar, setas para navegar), **Then** os botões prev/next recebem foco e são ativados por teclas direcionais.
7. **Given** a paginação no rodapé da lista, **When** o usuário está na página 2 ou superior, **Then** uma paginação idêntica também aparece no topo da lista de resultados.
8. **Given** que os dados do catálogo (tipos, comodidades, cidades) ainda estão carregando, **When** o sidebar é exibido, **Then** um skeleton placeholder é mostrado no lugar dos filtros, sem bloquear o carregamento dos imóveis.
---
### Edge Cases
- O que acontece quando o campo de busca textual é combinado com filtros de sidebar ativos ao mesmo tempo?
- Como o sistema lida com o parâmetro `q` contendo caracteres especiais ou SQL injection na URL?
- Como o badge "Novo" é calculado quando o servidor e o cliente estão em fusos horários diferentes?
- O que ocorre quando o carrossel tem apenas 1 foto (botões prev/next devem estar ocultos)?
- Como a paginação se comporta quando o total de resultados muda entre páginas (ex.: imóvel removido)?
- O que acontece se o usuário chegar na página 5 via link e a busca atual só tiver 3 páginas?
- Como a visualização em grade se comporta em dispositivos com largura entre 480640px?
---
## Requirements
### Functional Requirements
#### Sprint 1 — Correções Críticas
- **FR-001**: O sistema DEVE reestruturar o `PropertyRowCard` de forma que nenhum elemento `<button>` ou `<a>` esteja aninhado dentro de outro elemento `<a>`, garantindo HTML semântico válido.
- **FR-002**: Os botões prev/next do carrossel de fotos DEVEM ser visíveis em dispositivos touch sem depender do estado de hover, utilizando visibilidade condicional por breakpoint.
- **FR-003**: O sistema DEVE capturar erros de rede no fetch de imóveis e exibir uma mensagem de erro com botão de "Tentar novamente" em lugar da listagem vazia silenciosa.
- **FR-004**: O `PropertyRowCard` DEVE ter layout responsivo que se adapte entre mobile (coluna única vertical) e desktop (horizontal), sem altura fixa que trunque o conteúdo em tablets (7681023px).
- **FR-005**: A página `/imoveis` DEVE exibir um campo de busca textual proeminente no topo da área de resultados que filtre imóveis por título, endereço, bairro ou código do imóvel.
- **FR-006**: A busca textual DEVE usar debounce de 400ms para evitar requisições excessivas ao backend.
- **FR-007**: O parâmetro `q` DEVE ser sincronizado com a URL (`/imoveis?q=termo`) para permitir compartilhamento e histórico do browser.
- **FR-008**: O backend DEVE aceitar o parâmetro `q` na rota `GET /api/v1/properties` e aplicar busca case-insensitive nos campos `title`, `address`, `code` e `neighborhood.name`.
#### Sprint 2 — Alto Valor
- **FR-009**: A página DEVE exibir um seletor de ordenação ao lado do contador de resultados com as opções: Relevância, Menor preço, Maior preço, Maior área, Mais recente.
- **FR-010**: A ordenação selecionada DEVE ser sincronizada com a URL via parâmetro `sort` e mantida ao trocar de página.
- **FR-011**: O backend DEVE aceitar o parâmetro `sort` na rota `GET /api/v1/properties` com os valores: `relevance`, `price_asc`, `price_desc`, `area_desc`, `newest`.
- **FR-012**: Quando houver filtros ativos, chips removíveis DEVEM aparecer acima da listagem, cada um representando um filtro ativo com botão `×` para remoção individual.
- **FR-013**: Quando houver 2 ou mais filtros ativos, um botão "Limpar tudo" DEVE aparecer ao lado dos chips.
- **FR-014**: A página DEVE oferecer toggle de visualização Lista/Grade, com visualização em Grade exibindo cards verticais em 13 colunas responsivas.
- **FR-015**: A preferência de visualização Lista/Grade DEVE ser persistida em `localStorage` e restaurada na próxima visita.
- **FR-016**: O estado vazio (zero resultados) DEVE exibir sugestões acionáveis de filtros relaxados, cada uma com a quantidade de imóveis que seria retornada.
- **FR-017**: A hierarquia visual dos CTAs nos cards DEVE ser: "Ver detalhes" (primário — fundo da cor da marca), "Entre em contato" (secundário — outline), "Comparar" (terciário — ghost).
#### Sprint 3 — Refinamentos
- **FR-018**: Os cards DEVEM entrar na tela com animação fade-in-up com atraso crescente (stagger de ~40ms por card) a cada carregamento de nova página.
- **FR-019**: A paginação DEVE exibir o indicador de posição "Exibindo XY de Z imóveis".
- **FR-020**: Um botão flutuante "Voltar ao topo" DEVE aparecer após scroll de 400px e scroll suavemente ao topo ao ser clicado.
- **FR-021**: Imóveis com `is_featured = true` DEVEM exibir um badge "Destaque" sobreposto à foto do card.
- **FR-022**: Imóveis criados nos últimos 7 dias DEVEM exibir um badge "Novo" sobreposto à foto do card.
- **FR-023**: O carrossel de fotos DEVE suportar navegação por teclado: Tab para focar nos botões prev/next, setas direcionais para navegar entre slides.
- **FR-024**: A paginação DEVE aparecer também no topo da listagem de resultados (além do rodapé já existente).
- **FR-025**: O sidebar de filtros DEVE exibir um skeleton placeholder enquanto os dados do catálogo (tipos, comodidades, cidades) ainda estão carregando, sem bloquear a exibição dos imóveis.
- **FR-026**: O carrossel DEVE renderizar apenas o slide atual e os slides adjacentes (±1), evitando renderizar todas as fotos no DOM simultaneamente.
### Key Entities
- **Imóvel (Property)**: Unidade de listagem com título, endereço, código, bairro, tipo/subtipo, preço, área, fotos, flags `is_featured` e data de criação.
- **Filtros Ativos**: Estado derivado dos parâmetros de URL que representa a combinação atual de filtros aplicados pelo usuário, incluindo `q` (busca textual) e `sort` (ordenação).
- **Chip de Filtro**: Representação visual de um filtro ativo individual, removível de forma independente.
- **Preferência de Visualização**: Configuração do usuário (Lista ou Grade) persistida localmente entre sessões.
---
## Success Criteria
### Measurable Outcomes
- **SC-001**: Usuários em dispositivos mobile conseguem navegar pelas fotos do carrossel em 100% dos cards, sem depender de interação de hover.
- **SC-002**: Em caso de falha de rede, 100% das tentativas de carregamento resultam em mensagem de erro visível com opção de retentativa — zero falhas silenciosas.
- **SC-003**: Usuários com intenção específica (endereço, código ou bairro) conseguem filtrar resultados pela busca textual em menos de 5 segundos após digitar o termo.
- **SC-004**: A estrutura HTML da listagem não contém nenhum elemento interativo aninhado ilegalmente (`<button>` dentro de `<a>` ou vice-versa), validada por ferramentas automáticas de lint.
- **SC-005**: Usuários em tablets (7681023px) conseguem ler todas as informações de um card sem conteúdo truncado ou cortado por altura fixa.
- **SC-006**: Usuários conseguem identificar qual CTA é primário em menos de 3 segundos ao olhar para um card de imóvel.
- **SC-007**: Usuários com filtros aplicados conseguem identificar e remover qualquer filtro individual sem abrir o sidebar.
- **SC-008**: A preferência de visualização (Lista/Grade) é mantida entre sessões — 100% de consistência no retorno ao site.
- **SC-009**: O estado vazio apresenta ao menos 2 sugestões acionáveis de relaxamento de filtros, cada uma com contagem de resultados esperados.
- **SC-010**: Navegação completa da listagem (busca, filtros, ordenação, paginação, visualização do card) é realizável inteiramente por teclado, sem necessidade de mouse.
---
## Assumptions
- O campo `is_featured` já existe no modelo `Property` do backend ou pode ser adicionado via migration sem impacto em dados existentes.
- A data de criação (`created_at`) já existe no modelo `Property` e é preenchida automaticamente.
- O campo `code` (código do imóvel) já existe no modelo `Property` e é único.
- O sistema de filtros sincronizados com URL (`filtersToParams`) já está implementado e será estendido para incluir os novos parâmetros `q` e `sort`.
- O banco de dados suporta busca case-insensitive (`ILIKE`) nos campos relevantes sem necessidade de extensão adicional.
- A preferência de visualização (Lista/Grade) é armazenada em `localStorage` — não há necessidade de sincronização com conta de usuário logado nesta versão.
- O componente `PropertyGridCard` (modo grade) é um novo componente a ser criado; o `PropertyRowCard` existente não será removido.
- Animações de entrada respeitam a preferência do sistema `prefers-reduced-motion` — usuários que optaram por menos movimento não verão as animações.
- O indicador de posição na paginação ("Exibindo XY de Z imóveis") usa os dados já retornados pela API (`total`, `page`, `per_page`).
- O botão "Voltar ao topo" não conflita com a `ComparisonBar` existente — quando a barra de comparação está visível, o botão é posicionado acima dela.
- As sugestões do estado vazio são calculadas com requisições paralelas ao backend com filtros relaxados — não requerem endpoint dedicado.
- Nenhuma mudança em autenticação, permissões ou dados de usuário logado está no escopo desta feature.