426 lines
26 KiB
Markdown
426 lines
26 KiB
Markdown
---
|
||
description: "Tasks para a feature 023 - Melhorias UX/UI — Listagem de Imóveis"
|
||
---
|
||
|
||
# Tasks: Melhorias UX/UI — Listagem de Imóveis (023)
|
||
|
||
**Input**: Design documents de `specs/023-ux-melhorias-imoveis/`
|
||
**Prerequisites**: plan.md ✅ · spec.md ✅ · data-model.md ✅ · contracts/properties-api.md ✅ · auditoria: specs/022-ux-audit-imoveis/ux-audit.md ✅
|
||
**Sem migrations** — todos os campos usados já existem no modelo `Property`
|
||
|
||
---
|
||
|
||
## Format: `[ID] [P?] [Story?] Description — arquivo`
|
||
|
||
- **[P]**: Pode executar em paralelo (arquivo diferente, sem bloqueadores incompletos)
|
||
- **[Story]**: User story correspondente (US1–US8)
|
||
- Arquivo exato indicado em cada task
|
||
- **Sprint** de cada fase indicado no cabeçalho
|
||
|
||
---
|
||
|
||
## Phase 1: Foundational — Backend (Bloqueador de testes de integração)
|
||
|
||
**Sprint**: Pré-sprint (deve preceder o início do Sprint 1)
|
||
**Purpose**: Adicionar `q` e `sort` na rota existente `GET /api/v1/properties`. Sem migration — campos `title`, `address`, `code`, `neighborhood_id`, `price`, `area_m2`, `created_at`, `is_featured` já existem. Este phase não tem dependências de frontend.
|
||
|
||
**⚠️ CRÍTICO**: As tasks T003–T010 do Sprint 1 que dependem do backend (integração de busca textual) requerem T001 completo. As tasks de refactor de frontend (T004–T007) podem ser iniciadas em paralelo com T001/T002.
|
||
|
||
- [ ] T001 Adicionar parâmetros `q` (busca ILIKE em `title`, `address`, `code`, `neighborhood.name` via `outerjoin` com `aliased(Neighborhood)`) e `sort` (whitelist com `sort_map`) na rota `GET /api/v1/properties` em `backend/app/routes/properties.py` — sanitização de `q`: `.strip()` + truncamento a 200 chars; `sort` com fallback para `created_at.desc()`
|
||
|
||
**Critérios de aceitação**:
|
||
- `GET /api/v1/properties?q=Jardins` retorna apenas imóveis com "Jardins" no título, endereço, código ou bairro
|
||
- `GET /api/v1/properties?sort=price_asc` retorna imóveis em ordem crescente de preço
|
||
- `GET /api/v1/properties?sort=invalido` retorna imóveis na ordem padrão (sem erro 400/500)
|
||
- `GET /api/v1/properties?q=<script>alert(1)</script>` não causa SQL injection nem 500
|
||
|
||
- [ ] T002 [P] Criar/atualizar testes pytest em `backend/tests/test_properties.py` para validar `q` (busca por título, por bairro, por código) e `sort` (price_asc retorna menor primeiro, area_desc retorna maior primeiro, valor desconhecido usa default) — fixture com ao menos 3 imóveis de preços distintos
|
||
|
||
**Critérios de aceitação**:
|
||
- `test_search_by_title_q`, `test_search_by_neighborhood_q`, `test_search_by_code_q` passam
|
||
- `test_sort_price_asc`, `test_sort_price_desc`, `test_sort_area_desc`, `test_sort_unknown_fallback` passam
|
||
- `pytest tests/test_properties.py -v` termina verde sem erros
|
||
|
||
**Checkpoint**: `curl "http://localhost:5000/api/v1/properties?q=test&sort=price_asc"` retorna 200 com `items` e `total`.
|
||
|
||
---
|
||
|
||
## Phase 2: Sprint 1 — Correções Críticas (P1)
|
||
|
||
**Sprint**: 1
|
||
**Purpose**: Resolver os 5 problemas 🔴 críticos identificados na auditoria: semântica HTML inválida (FR-001), carrossel inacessível em mobile (FR-002), ausência de tratamento de erro de rede (FR-003), layout fixo em tablets (FR-004) e campo de busca textual (FR-005 a FR-008).
|
||
|
||
**Independent Test (US1)**: Abrir `/imoveis` em mobile, navegar pelas fotos tocando em prev/next, simular falha de rede e verificar mensagem de erro. Inspecionar DOM e confirmar ausência de `<button>` dentro de `<a>`.
|
||
|
||
**Independent Test (US2)**: Digitar "Barra Funda" no campo de busca, verificar que URL muda para `/imoveis?q=Barra+Funda` e resultados são filtrados. Limpar busca e verificar retorno ao estado anterior.
|
||
|
||
---
|
||
|
||
### US1 — Correções Críticas de Usabilidade
|
||
|
||
- [ ] T003 [US1] Refatorar estrutura HTML do `frontend/src/components/PropertyRowCard.tsx` — substituir o `<Link>` que envolve toda a seção de informações por um overlay absoluto (`className="absolute inset-0" tabIndex={-1} aria-label="Ver detalhes: {title}"`); mover botões "Comparar" e "Entre em contato" para fora do `<Link>` com `relative z-index: 10`; envolver o card em `<article className="relative group ...">` — **este refactor é pré-requisito para T005, T015 e T019**
|
||
|
||
**Critérios de aceitação**:
|
||
- Nenhum `<button>` aninhado dentro de `<a>` no DOM inspecionado
|
||
- Clicar no card (fora dos botões) navega para a página de detalhes
|
||
- Clicar em "Comparar" ou "Entre em contato" não dispara navegação
|
||
- Leitor de tela anuncia o link com `aria-label` correto
|
||
|
||
- [ ] T004 [US1] Corrigir visibilidade dos botões prev/next do carrossel em dispositivos touch em `frontend/src/components/PropertyRowCard.tsx` — trocar `opacity-0 group-hover:opacity-100` por `opacity-100 sm:opacity-0 sm:group-hover:opacity-100` nos botões de navegação do carrossel (visível sempre em mobile, hover-only em desktop)
|
||
|
||
**Critérios de aceitação**:
|
||
- Em viewport ≤640px, botões prev/next são visíveis sem toque/hover
|
||
- Em viewport ≥640px, botões prev/next aparecem apenas com hover no card
|
||
- Botões com apenas 1 foto ficam ocultos (`photos.length <= 1`)
|
||
|
||
- [ ] T005 [US1] Corrigir layout responsivo do card em `frontend/src/components/PropertyRowCard.tsx` — remover `h-[220px]` fixo do article e `w-[340px]` fixo da imagem; usar `flex flex-col sm:flex-row sm:h-[220px]` no article e `w-full h-48 sm:w-[280px] sm:h-full lg:w-[340px]` na div da imagem — garante que em tablets (768–1023px) o conteúdo não seja truncado
|
||
|
||
**Critérios de aceitação**:
|
||
- Em viewport 768px, o card exibe título, endereço e stats sem corte de texto
|
||
- Em viewport 1024px, o card mantém layout horizontal com proporções corretas
|
||
- Em viewport 375px (mobile), o card exibe layout em coluna única sem overflow
|
||
|
||
- [ ] T006 [US1] Implementar tratamento de erro de rede em `frontend/src/pages/PropertiesPage.tsx` — adicionar `const [error, setError] = useState<string | null>(null)`; no bloco `catch` do `fetchProperties`, definir `setError('Não foi possível carregar os imóveis. Tente novamente.')` e limpar em nova tentativa; renderizar mensagem de erro com botão "Tentar novamente" que chama `fetchProperties()` no lugar da listagem vazia silenciosa
|
||
|
||
**Critérios de aceitação**:
|
||
- Com API inacessível (ex: container parado), mensagem de erro é exibida
|
||
- Botão "Tentar novamente" dispara novo request ao ser clicado
|
||
- Erro é limpo quando um request subsequente é bem-sucedido
|
||
- Skeleton de loading não aparece durante o estado de erro
|
||
|
||
- [ ] T007 [US1] Adicionar indicador visual de carregamento sutil em `frontend/src/pages/PropertiesPage.tsx` — aplicar `className={loading ? 'opacity-50 pointer-events-none' : 'opacity-100'}` com `transition-opacity duration-150` na div que envolve os cards; manter cards anteriores visíveis com opacidade reduzida ao invés de mostrar skeleton completo ao trocar filtros
|
||
|
||
**Critérios de aceitação**:
|
||
- Ao mudar qualquer filtro, os cards anteriores ficam com opacidade reduzida imediatamente (antes do response da API)
|
||
- Ao completar o request, opacidade volta a 100% com transição suave
|
||
- Cliques nos cards são bloqueados durante loading (`pointer-events-none`)
|
||
|
||
---
|
||
|
||
### US2 — Campo de Busca Textual
|
||
|
||
- [ ] T008 [P] [US2] Adicionar `q?: string` ao tipo `PropertyFilters` e criar tipo `SortOption = 'relevance' | 'price_asc' | 'price_desc' | 'area_desc' | 'newest'` com `sort?: SortOption` em `frontend/src/services/properties.ts` — incluir ambos como query params na chamada Axios
|
||
|
||
**Critérios de aceitação**:
|
||
- Compilação TypeScript sem erros após a alteração
|
||
- Chamada `getProperties({ q: 'Jardins', sort: 'price_asc' })` gera URL `?q=Jardins&sort=price_asc`
|
||
- `q` vazio ou undefined não adiciona `?q=` na URL (usar `params` do Axios com valores falsy omitidos)
|
||
|
||
- [ ] T009 [P] [US2] Criar `frontend/src/components/SearchBar.tsx` — input controlado com placeholder "Buscar por endereço, bairro ou código...", ícone de lupa, debounce de 400ms via `useEffect` + `setTimeout`, botão `×` visível quando há texto, limpa o campo e dispara `onSearch('')` ao clicar; props: `value: string`, `onSearch: (q: string) => void`
|
||
|
||
**Critérios de aceitação**:
|
||
- Digitar "Jard" não dispara chamada imediata; após 400ms de inatividade, `onSearch('Jard')` é chamado
|
||
- Botão `×` aparece quando `value.length > 0` e desaparece quando vazio
|
||
- Clicar em `×` chama `onSearch('')` e limpa o input
|
||
- Campo tem `role="search"` e `aria-label="Buscar imóveis"`
|
||
|
||
- [ ] T010 [US2] Integrar `SearchBar` em `frontend/src/pages/PropertiesPage.tsx` — posicionar acima do header de resultados (contador + sort), sincronizar com parâmetro `q` da URL via `useSearchParams`, resetar `page` para 1 ao mudar a busca, exibir estado vazio específico com sugestão de termos quando busca não retorna resultados
|
||
|
||
**Critérios de aceitação**:
|
||
- Digitar "Barra Funda" atualiza URL para `/imoveis?q=Barra+Funda` sem reload completo
|
||
- Compartilhar URL com `?q=Jardins` exibe resultados filtrados para o destinatário
|
||
- Limpar o campo remove `q` da URL e restaura listagem sem filtro textual
|
||
- Busca + filtros de sidebar funcionam combinados (AND lógico)
|
||
|
||
**Checkpoint Sprint 1**: Abrir `/imoveis`, inspecionar DOM sem `<button>` dentro de `<a>`, navegar fotos em mobile, testar busca por bairro, simular rede off e ver mensagem de erro.
|
||
|
||
---
|
||
|
||
## Phase 3: Sprint 2 — Alto Valor de Conversão (P2)
|
||
|
||
**Sprint**: 2
|
||
**Purpose**: Adicionar funcionalidades que aumentam diretamente a taxa de conversão: ordenação de resultados (FR-009 a FR-011), chips de filtros ativos (FR-012, FR-013), toggle Lista/Grade (FR-014, FR-015), estado vazio rico (FR-016) e hierarquia visual de CTAs (FR-017).
|
||
|
||
**Dependências**: T003 (refactor do card) deve estar completo antes de T019 (CTAs). T008 (PropertyFilters) deve estar completo antes de T011. T015 (PropertyGridCard) deve estar completo antes de T019 aplicar a este.
|
||
|
||
---
|
||
|
||
### US3 — Ordenação de Resultados
|
||
|
||
- [ ] T011 [US3] Adicionar seletor de ordenação no header de resultados em `frontend/src/pages/PropertiesPage.tsx` — `<select>` com 5 opções mapeadas para `SortOption`, ao lado do contador "X imóveis encontrados"; sincronizar `sort` com URL via `useSearchParams`; resetar `page` para 1 ao mudar ordenação; manter `sort` ao trocar de página
|
||
|
||
**Critérios de aceitação**:
|
||
- Selecionar "Menor preço" atualiza URL para `?sort=price_asc` e reordena a listagem
|
||
- Navegar para a página 2 com `sort=price_asc` mantém a ordenação na nova página
|
||
- Compartilhar URL com `?sort=newest` exibe mesma ordenação para o destinatário
|
||
- Opção "Relevância" é a default quando `sort` está ausente na URL
|
||
|
||
---
|
||
|
||
### US4 — Chips de Filtros Ativos
|
||
|
||
- [ ] T012 [P] [US4] Criar `frontend/src/components/ActiveFiltersBar.tsx` — recebe `filters: PropertyFilters` e `catalogData` (tipos, cidades, bairros); deriva array de `ActiveFilterChip[]` com `key`, `label` legível e `onRemove: () => void`; renderiza chips com botão `×` usando `aria-label="Remover filtro {label}"`; exibe botão "Limpar tudo" apenas quando `chips.length >= 2`; não renderiza nada quando `chips.length === 0`
|
||
|
||
**Critérios de aceitação**:
|
||
- Com filtros `listing_type=aluguel` + `city_id=1` + `bedrooms_min=2`, renderiza 3 chips com labels legíveis
|
||
- Clicar no `×` do chip "São Paulo" remove apenas `city_id` dos filtros e dispara `onFilterChange`
|
||
- Botão "Limpar tudo" aparece com ≥2 chips e remove todos ao clicar
|
||
- Com zero filtros ativos, o componente não renderiza nenhum elemento no DOM
|
||
|
||
- [ ] T013 [US4] Integrar `ActiveFiltersBar` em `frontend/src/pages/PropertiesPage.tsx` — posicionar abaixo de `SearchBar`, acima do primeiro card; passar `filters` atual e callbacks de remoção individual por chave de filtro (`onRemove(key) => setFilters(prev => omit(prev, key))`)
|
||
|
||
**Critérios de aceitação**:
|
||
- Aplicar filtro de tipo + cidade exibe chips correspondentes acima dos resultados
|
||
- Remover chip via `×` atualiza a listagem sem apagar outros filtros ativos
|
||
- Chips desaparecem quando todos os filtros são removidos via "Limpar tudo"
|
||
|
||
---
|
||
|
||
### US5 — Toggle de Visualização Lista/Grade
|
||
|
||
- [ ] T014 [P] [US5] Criar `frontend/src/components/PropertyGridCard.tsx` — card vertical com foto em destaque (aspectRatio 4/3, `object-cover`), título, preço, badges básicos (quartos/área/vagas), `<Link to={/imoveis/${slug}}>` como overlay absoluto (`tabIndex={-1}`), botão "Ver detalhes" como CTA primário visível; sem botões "Comparar" e "Entre em contato" (modo grade prioriza descoberta)
|
||
|
||
**Critérios de aceitação**:
|
||
- Card renderiza foto, título e preço sem truncamento em qualquer largura de coluna
|
||
- Clicar no card (fora do botão) navega para a página de detalhes
|
||
- Clicar em "Ver detalhes" navega para a página de detalhes
|
||
- Sem `<button>` aninhado em `<a>` no DOM
|
||
|
||
- [ ] T015 [US5] Adicionar toggle Lista/Grade no header de `frontend/src/pages/PropertiesPage.tsx` — estado `viewMode: ViewMode` inicializado de `localStorage.getItem('imoveis_view_mode') ?? 'list'`; dois botões de toggle com ícones (≡ Lista / ⊞ Grade) com `aria-pressed`; grid responsivo quando grade (`grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-4`) vs flex-col quando lista; persistir no `localStorage` ao mudar
|
||
|
||
**Critérios de aceitação**:
|
||
- Clicar em "Grade" alterna layout para grid de 1–3 colunas responsivo com `PropertyGridCard`
|
||
- Recarregar a página mantém o modo de visualização selecionado (localStorage)
|
||
- Botão ativo recebe indicação visual distinta (`aria-pressed="true"`)
|
||
- Navegação para detalhe funciona em ambos os modos
|
||
|
||
---
|
||
|
||
### US6 — Estado Vazio com Sugestões
|
||
|
||
- [ ] T016 [P] [US6] Criar `frontend/src/components/EmptyStateWithSuggestions.tsx` — recebe `currentFilters: PropertyFilters` e `onApplySuggestion: (filters: PropertyFilters) => void`; exibe mensagem "Nenhum imóvel encontrado" + lista de sugestões acionáveis (ex: remover filtro de bairro, ampliar faixa de preço, reduzir mínimo de quartos), cada sugestão com contagem de imóveis seria encontrada (recebida via prop `suggestions: EmptyStateSuggestion[]`); botão "Limpar todos os filtros"
|
||
|
||
**Critérios de aceitação**:
|
||
- Exibe ao menos 3 sugestões quando há filtros ativos
|
||
- Clicar em sugestão chama `onApplySuggestion` com filtros relaxados e atualiza listagem
|
||
- Botão "Limpar todos os filtros" remove todos os filtros e retorna resultados
|
||
- Contagem de imóveis por sugestão é exibida (ex: "→ 12 imóveis disponíveis")
|
||
|
||
- [ ] T017 [US6] Integrar `EmptyStateWithSuggestions` em `frontend/src/pages/PropertiesPage.tsx` — quando `result.total === 0` e `!loading`, fazer 3 requests paralelos (`Promise.all`) com filtros relaxados (sem `neighborhood_id`, sem `bedrooms_min`, sem `price_max`) para calcular contagens; passar `suggestions` para o componente; substituir o estado vazio simples atual
|
||
|
||
**Critérios de aceitação**:
|
||
- Com filtros impossíveis (ex: `bedrooms_min=10`), estado vazio mostra sugestões com contagens reais
|
||
- Requests de sugestões são paralelos (não sequenciais), sem bloquear a UI
|
||
- Clicar numa sugestão atualiza os filtros ativos e exibe os resultados correspondentes
|
||
- Quando não há filtros ativos e o resultado é vazio, exibe mensagem genérica sem sugestões
|
||
|
||
---
|
||
|
||
### US7 — Hierarquia Visual de CTAs no Card
|
||
|
||
- [ ] T018 [US7] Atualizar hierarquia visual dos CTAs em `frontend/src/components/PropertyRowCard.tsx` — "Ver detalhes" como `<Link>` com estilo primário (fundo `var(--color-brand)`, texto branco); "Entre em contato" como `<button>` com estilo outline (borda `var(--color-brand)`, background transparente); "Comparar" como `<button>` com estilo ghost (sem borda, apenas texto muted com hover sutil); manter todos fora do `<Link>` overlay (depende de T003)
|
||
|
||
**Critérios de aceitação**:
|
||
- "Ver detalhes" tem fundo colorido e destaque visual primário
|
||
- "Entre em contato" tem borda colorida sem fundo (outline)
|
||
- "Comparar" tem aparência discreta sem borda (ghost/minimal)
|
||
- Hierarquia mantida em viewport mobile (375px)
|
||
- Nenhum `<button>` dentro de `<a>` no DOM
|
||
|
||
**Checkpoint Sprint 2**: Aplicar filtros de tipo + cidade + quartos, verificar chips aparecem. Selecionar ordenação por preço. Alternar para grade. Aplicar filtro impossível e verificar sugestões. Confirmar hierarquia visual dos CTAs.
|
||
|
||
---
|
||
|
||
## Phase 4: Sprint 3 — Refinamentos de Qualidade (P3)
|
||
|
||
**Sprint**: 3
|
||
**Purpose**: Polimento percebido que aumenta a sensação de qualidade do produto sem bloquear fluxos de uso: animações (FR-018), indicador de paginação (FR-019), scroll-to-top (FR-020), badges de status (FR-021, FR-022), teclado no carrossel (FR-023), paginação no topo (FR-024), skeleton no sidebar (FR-025).
|
||
|
||
**Independent Test (US8)**: Navegar para página 2, verificar "Exibindo X–Y de Z imóveis"; pressionar Tab no carrossel e usar setas para navegar; verificar badge "Destaque" em imóvel com `is_featured=true`.
|
||
|
||
---
|
||
|
||
### US8 — Refinamentos de Qualidade
|
||
|
||
- [ ] T019 [US8] Adicionar keyframe `@keyframes fade-in-up` em `frontend/src/index.css` (translateY de 8px→0, opacity 0→1, duration 300ms ease-out) e aplicar `style={{ animationDelay: \`${index * 40}ms\` }}` nos cards mapeados em `frontend/src/pages/PropertiesPage.tsx` para stagger; resetar animação ao trocar de página (chave no `key` do item)
|
||
|
||
**Critérios de aceitação**:
|
||
- Cards entram com animação sutil ao carregar nova página
|
||
- Stagger visível entre cards consecutivos (~40ms de diferença)
|
||
- Animação não ocorre durante loading (cards com opacidade reduzida) — apenas após novo resultado
|
||
- Sem `prefers-reduced-motion` override (adicionar `@media (prefers-reduced-motion: reduce)` sem animação)
|
||
|
||
- [ ] T020 [P] [US8] Adicionar indicador de posição "Exibindo X–Y de Z imóveis" em `frontend/src/pages/PropertiesPage.tsx` — calcular `from = (page - 1) * perPage + 1`, `to = Math.min(page * perPage, total)`; renderizar próximo ao contador de resultados ou acima da paginação inferior
|
||
|
||
**Critérios de aceitação**:
|
||
- Na página 1 com 16 por página e 45 total: exibe "Exibindo 1–16 de 45 imóveis"
|
||
- Na página 3: exibe "Exibindo 33–45 de 45 imóveis"
|
||
- Não exibir quando `total === 0` (estado vazio)
|
||
|
||
- [ ] T021 [P] [US8] Criar `frontend/src/components/ScrollToTopButton.tsx` — botão flutuante fixo (`fixed bottom-6 right-6`), aparece quando `scrollY > 400` via `useEffect` com listener de `scroll`, chama `window.scrollTo({ top: 0, behavior: 'smooth' })` ao clicar; integrar em `frontend/src/pages/PropertiesPage.tsx` como filho direto da página
|
||
|
||
**Critérios de aceitação**:
|
||
- Botão fica oculto antes de 400px de scroll e aparece após esse limiar
|
||
- Clicar no botão rola suavemente para o topo
|
||
- Botão tem `aria-label="Voltar ao topo"` para acessibilidade
|
||
- Listener de scroll é removido no cleanup do `useEffect` (sem leak)
|
||
|
||
- [ ] T022 [US8] Adicionar badges "Destaque" e "Novo" sobrepostos à foto em `frontend/src/components/PropertyRowCard.tsx` e `frontend/src/components/PropertyGridCard.tsx` — badge "Destaque" quando `property.is_featured === true` (fundo âmbar, `⭐ Destaque`); badge "Novo" quando `created_at` for de até 7 dias atrás — calculado no frontend: `Date.now() - new Date(created_at).getTime() < 7 * 24 * 60 * 60 * 1000`; posicionar `absolute top-2 left-2` na div da foto
|
||
|
||
**Critérios de aceitação**:
|
||
- Imóvel com `is_featured=true` exibe badge "⭐ Destaque" na foto
|
||
- Imóvel com `created_at` de ontem exibe badge "Novo" na foto
|
||
- Imóvel com `created_at` de 8 dias atrás não exibe badge "Novo"
|
||
- Ambos os badges podem coexistir no mesmo card
|
||
|
||
- [ ] T023 [US8] Adicionar navegação por teclado no carrossel de `frontend/src/components/PropertyRowCard.tsx` — botões prev/next devem ser focáveis via Tab; ao focar qualquer botão do carrossel, adicionar `onKeyDown` que responde a `ArrowLeft` (prev) e `ArrowRight` (next); `aria-label="Foto anterior"` / `"Próxima foto"` nos botões
|
||
|
||
**Critérios de aceitação**:
|
||
- Tab navega para os botões prev/next do carrossel
|
||
- Pressionar ArrowRight no botão next avança o slide
|
||
- Pressionar ArrowLeft no botão prev retrocede o slide
|
||
- Botões com 1 única foto ficam com `aria-disabled="true"` e não respondem a teclado
|
||
|
||
- [ ] T024 [P] [US8] Adicionar paginação duplicada no topo da listagem em `frontend/src/pages/PropertiesPage.tsx` — renderizar o mesmo componente de paginação (já existente) acima do primeiro card, com `aria-label="Paginação superior"`; visível apenas quando `result.pages > 1`
|
||
|
||
**Critérios de aceitação**:
|
||
- Com mais de 1 página de resultados, paginação aparece no topo E no rodapé
|
||
- Com 1 página apenas, apenas o rodapé é exibido
|
||
- Ambas as paginações atualizam a página ao mesmo tempo (estado compartilhado)
|
||
|
||
- [ ] T025 [P] [US8] Adicionar skeleton de carregamento no `frontend/src/components/FilterSidebar.tsx` — exibir placeholders animados (`animate-pulse bg-surface rounded`) no lugar dos filtros de tipo, cidade, bairro e comodidades enquanto `catalogLoading === true`; a listagem de imóveis continua carregando independentemente
|
||
|
||
**Critérios de aceitação**:
|
||
- Enquanto `catalogLoading` for true, skeleton é exibido no sidebar sem bloquear a listagem
|
||
- Ao completar o carregamento, skeleton é substituído pelos filtros reais sem flash
|
||
- Skeleton tem mesma altura aproximada dos filtros para evitar CLS
|
||
|
||
**Checkpoint Sprint 3**: Navegar para página 2 e verificar indicador de posição. Rolar 400px e verificar botão flutuante. Verificar badge em imóvel com `is_featured=true`. Testar Tab + setas no carrossel.
|
||
|
||
---
|
||
|
||
## Phase 5: Polish & Verificação Final
|
||
|
||
**Purpose**: Validação cruzada de semântica HTML, acessibilidade, TypeScript e testes backend.
|
||
|
||
- [ ] T026 Inspecionar DOM de `/imoveis` no browser e verificar ausência de `<button>` dentro de `<a>` em todos os cards (lista e grade) — corrigir qualquer instância remanescente em `frontend/src/components/PropertyRowCard.tsx` ou `frontend/src/components/PropertyGridCard.tsx`
|
||
|
||
**Critérios de aceitação**:
|
||
- DevTools → Elements: nenhum seletor `a button`, `a [role=button]` encontrado
|
||
- Validação HTML5 sem erros de aninhamento inválido
|
||
|
||
- [ ] T027 Executar testes backend e verificar build TypeScript sem erros — `docker-compose exec backend uv run pytest tests/test_properties.py -v` deve terminar verde; `docker-compose exec frontend npx tsc --noEmit` deve terminar sem erros
|
||
|
||
**Critérios de aceitação**:
|
||
- Todos os testes pytest de `test_properties.py` passam
|
||
- Compilação TypeScript sem erros de tipo
|
||
- Nenhum `console.error` no browser ao carregar `/imoveis`
|
||
|
||
---
|
||
|
||
## Dependency Graph
|
||
|
||
```
|
||
T001 (backend q+sort)
|
||
└─► T002 (testes backend)
|
||
└─► T010 (integração SearchBar — valida endpoint)
|
||
|
||
T003 (refactor HTML card)
|
||
└─► T004 (carrossel mobile — mesmo arquivo)
|
||
└─► T005 (layout tablet — mesmo arquivo)
|
||
└─► T018 (CTAs — reestrutura botões)
|
||
└─► T022 (badges — adiciona na foto já reestruturada)
|
||
└─► T023 (teclado carrossel — botões reestruturados)
|
||
|
||
T008 (PropertyFilters tipos)
|
||
└─► T009 (SearchBar usa onSearch callback)
|
||
└─► T010 (PropertiesPage usa q no state)
|
||
└─► T011 (PropertiesPage usa sort no state)
|
||
└─► T012 (ActiveFiltersBar usa PropertyFilters)
|
||
└─► T016 (EmptyStateWithSuggestions usa PropertyFilters)
|
||
|
||
T014 (PropertyGridCard — novo componente)
|
||
└─► T015 (toggle grade renderiza PropertyGridCard)
|
||
└─► T022 (badges adicionados em PropertyGridCard)
|
||
|
||
T006 (error state PropertiesPage)
|
||
└─► T007 (opacity loading — mesmo arquivo, mesma sessão)
|
||
└─► T010 (integração SearchBar — mesmo arquivo)
|
||
└─► T011 (seletor sort — mesmo arquivo)
|
||
└─► T013 (integra ActiveFiltersBar — mesmo arquivo)
|
||
└─► T015 (toggle grade — mesmo arquivo)
|
||
└─► T017 (integra EmptyState — mesmo arquivo)
|
||
└─► T019 (animação — mesmo arquivo)
|
||
└─► T020 (indicador posição — mesmo arquivo)
|
||
└─► T024 (paginação top — mesmo arquivo)
|
||
```
|
||
|
||
---
|
||
|
||
## Parallel Execution Examples
|
||
|
||
### Sprint 1 — Paralelo possível
|
||
|
||
```
|
||
Thread A: T001 → T002
|
||
Thread B: T003 → T004 → T005
|
||
Thread C: T008 → T009
|
||
Thread D: T006 → T007
|
||
```
|
||
→ Após threads B e C concluídos: T010 (integra SearchBar em PropertiesPage com q sincronizado)
|
||
|
||
### Sprint 2 — Paralelo possível
|
||
|
||
```
|
||
Thread A: T011 (sort selector em PropertiesPage)
|
||
Thread B: T012 (ActiveFiltersBar — novo arquivo)
|
||
Thread C: T014 (PropertyGridCard — novo arquivo)
|
||
Thread D: T016 (EmptyStateWithSuggestions — novo arquivo)
|
||
```
|
||
→ Após thread B: T013 (integra ActiveFiltersBar em PropertiesPage)
|
||
→ Após thread C: T015 (toggle grade em PropertiesPage)
|
||
→ Após thread D: T017 (integra EmptyState em PropertiesPage)
|
||
→ Após T003 completo: T018 (CTAs em PropertyRowCard)
|
||
|
||
### Sprint 3 — Paralelo possível
|
||
|
||
```
|
||
Thread A: T019 (animações — PropertiesPage + index.css)
|
||
Thread B: T020 (indicador posição — PropertiesPage)
|
||
Thread C: T021 (ScrollToTopButton — novo arquivo)
|
||
Thread D: T024 (paginação top — PropertiesPage)
|
||
Thread E: T025 (skeleton sidebar — FilterSidebar)
|
||
```
|
||
→ Após T003+T014: T022 (badges em ambos os cards)
|
||
→ Após T003: T023 (teclado carrossel em PropertyRowCard)
|
||
|
||
---
|
||
|
||
## Implementation Strategy
|
||
|
||
### MVP Scope (Sprint 1 apenas)
|
||
|
||
Para uma entrega incremental mínima que resolve os problemas críticos bloqueadores de conversão:
|
||
- **T001** + **T002**: Backend com `q` e `sort`
|
||
- **T003** + **T004** + **T005**: Card sem HTML inválido e funcional em mobile/tablet
|
||
- **T006** + **T007**: Tratamento de erro e feedback de loading
|
||
- **T008** + **T009** + **T010**: Campo de busca textual funcional
|
||
|
||
Resultado: `/imoveis` sem erros críticos de HTML, funcional em mobile/tablet, com busca textual e tratamento de erros.
|
||
|
||
### Sprint 2 — Funcionalidades de Conversão
|
||
|
||
Adicionar T011 (ordenação), T012–T013 (chips), T014–T015 (grade), T016–T017 (empty state rico), T018 (CTAs).
|
||
|
||
### Sprint 3 — Polimento
|
||
|
||
Adicionar T019–T025 (animações, badges, teclado, scroll-to-top, paginação dupla, skeleton sidebar).
|
||
|
||
---
|
||
|
||
## Summary
|
||
|
||
| Métrica | Valor |
|
||
|---|---|
|
||
| Total de tasks | 27 |
|
||
| Sprint 1 (P1 — crítico) | T001–T010 (10 tasks) |
|
||
| Sprint 2 (P2 — alto valor) | T011–T018 (8 tasks) |
|
||
| Sprint 3 (P3 — refinamentos) | T019–T025 (7 tasks) |
|
||
| Polish | T026–T027 (2 tasks) |
|
||
| Tasks backend | T001, T002 (2 tasks) |
|
||
| Tasks frontend | T003–T025 (23 tasks) |
|
||
| Tasks de teste | T002 (pytest backend) |
|
||
| Tasks paralelizáveis [P] | T002, T008, T009, T012, T014, T016, T020, T021, T024, T025 |
|
||
| Novos componentes | SearchBar, PropertyGridCard, ActiveFiltersBar, EmptyStateWithSuggestions, ScrollToTopButton |
|
||
| Arquivos modificados | PropertyRowCard, PropertiesPage, FilterSidebar, services/properties.ts, index.css |
|
||
| Migrations de banco | Nenhuma |
|