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,254 @@
# 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.