26 KiB
| 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 emtitle,address,code,neighborhood.nameviaouterjoincomaliased(Neighborhood)) esort(whitelist comsort_map) na rotaGET /api/v1/propertiesembackend/app/routes/properties.py— sanitização deq:.strip()+ truncamento a 200 chars;sortcom fallback paracreated_at.desc()Critérios de aceitação:
GET /api/v1/properties?q=Jardinsretorna apenas imóveis com "Jardins" no título, endereço, código ou bairroGET /api/v1/properties?sort=price_ascretorna imóveis em ordem crescente de preçoGET /api/v1/properties?sort=invalidoretorna 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.pypara validarq(busca por título, por bairro, por código) esort(price_asc retorna menor primeiro, area_desc retorna maior primeiro, valor desconhecido usa default) — fixture com ao menos 3 imóveis de preços distintosCritérios de aceitação:
test_search_by_title_q,test_search_by_neighborhood_q,test_search_by_code_qpassamtest_sort_price_asc,test_sort_price_desc,test_sort_area_desc,test_sort_unknown_fallbackpassampytest tests/test_properties.py -vtermina 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>comrelative z-index: 10; envolver o card em<article className="relative group ...">— este refactor é pré-requisito para T005, T015 e T019Crité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-labelcorreto
- Nenhum
-
T004 [US1] Corrigir visibilidade dos botões prev/next do carrossel em dispositivos touch em
frontend/src/components/PropertyRowCard.tsx— trocaropacity-0 group-hover:opacity-100poropacity-100 sm:opacity-0 sm:group-hover:opacity-100nos 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— removerh-[220px]fixo do article ew-[340px]fixo da imagem; usarflex flex-col sm:flex-row sm:h-[220px]no article ew-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 truncadoCrité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— adicionarconst [error, setError] = useState<string | null>(null); no blococatchdofetchProperties, definirsetError('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 chamafetchProperties()no lugar da listagem vazia silenciosaCrité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— aplicarclassName={loading ? 'opacity-50 pointer-events-none' : 'opacity-100'}comtransition-opacity duration-150na div que envolve os cards; manter cards anteriores visíveis com opacidade reduzida ao invés de mostrar skeleton completo ao trocar filtrosCrité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?: stringao tipoPropertyFilterse criar tipoSortOption = 'relevance' | 'price_asc' | 'price_desc' | 'area_desc' | 'newest'comsort?: SortOptionemfrontend/src/services/properties.ts— incluir ambos como query params na chamada AxiosCrité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 qvazio ou undefined não adiciona?q=na URL (usarparamsdo 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 viauseEffect+setTimeout, botão×visível quando há texto, limpa o campo e disparaonSearch('')ao clicar; props:value: string,onSearch: (q: string) => voidCritérios de aceitação:
- Digitar "Jard" não dispara chamada imediata; após 400ms de inatividade,
onSearch('Jard')é chamado - Botão
×aparece quandovalue.length > 0e desaparece quando vazio - Clicar em
×chamaonSearch('')e limpa o input - Campo tem
role="search"earia-label="Buscar imóveis"
- Digitar "Jard" não dispara chamada imediata; após 400ms de inatividade,
-
T010 [US2] Integrar
SearchBaremfrontend/src/pages/PropertiesPage.tsx— posicionar acima do header de resultados (contador + sort), sincronizar com parâmetroqda URL viauseSearchParams, resetarpagepara 1 ao mudar a busca, exibir estado vazio específico com sugestão de termos quando busca não retorna resultadosCritérios de aceitação:
- Digitar "Barra Funda" atualiza URL para
/imoveis?q=Barra+Fundasem reload completo - Compartilhar URL com
?q=Jardinsexibe resultados filtrados para o destinatário - Limpar o campo remove
qda URL e restaura listagem sem filtro textual - Busca + filtros de sidebar funcionam combinados (AND lógico)
- Digitar "Barra Funda" atualiza URL para
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 paraSortOption, ao lado do contador "X imóveis encontrados"; sincronizarsortcom URL viauseSearchParams; resetarpagepara 1 ao mudar ordenação; mantersortao trocar de páginaCritérios de aceitação:
- Selecionar "Menor preço" atualiza URL para
?sort=price_asce reordena a listagem - Navegar para a página 2 com
sort=price_ascmantém a ordenação na nova página - Compartilhar URL com
?sort=newestexibe mesma ordenação para o destinatário - Opção "Relevância" é a default quando
sortestá ausente na URL
- Selecionar "Menor preço" atualiza URL para
US4 — Chips de Filtros Ativos
-
T012 [P] [US4] Criar
frontend/src/components/ActiveFiltersBar.tsx— recebefilters: PropertyFiltersecatalogData(tipos, cidades, bairros); deriva array deActiveFilterChip[]comkey,labellegível eonRemove: () => void; renderiza chips com botão×usandoaria-label="Remover filtro {label}"; exibe botão "Limpar tudo" apenas quandochips.length >= 2; não renderiza nada quandochips.length === 0Crité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 apenascity_iddos filtros e disparaonFilterChange - 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
- Com filtros
-
T013 [US4] Integrar
ActiveFiltersBaremfrontend/src/pages/PropertiesPage.tsx— posicionar abaixo deSearchBar, acima do primeiro card; passarfiltersatual 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— estadoviewMode: ViewModeinicializado delocalStorage.getItem('imoveis_view_mode') ?? 'list'; dois botões de toggle com ícones (≡ Lista / ⊞ Grade) comaria-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 nolocalStorageao mudarCrité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
- Clicar em "Grade" alterna layout para grid de 1–3 colunas responsivo com
US6 — Estado Vazio com Sugestões
-
T016 [P] [US6] Criar
frontend/src/components/EmptyStateWithSuggestions.tsx— recebecurrentFilters: PropertyFilterseonApplySuggestion: (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 propsuggestions: 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
onApplySuggestioncom 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
EmptyStateWithSuggestionsemfrontend/src/pages/PropertiesPage.tsx— quandoresult.total === 0e!loading, fazer 3 requests paralelos (Promise.all) com filtros relaxados (semneighborhood_id, sembedrooms_min, semprice_max) para calcular contagens; passarsuggestionspara o componente; substituir o estado vazio simples atualCrité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
- Com filtros impossíveis (ex:
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 (fundovar(--color-brand), texto branco); "Entre em contato" como<button>com estilo outline (bordavar(--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-upemfrontend/src/index.css(translateY de 8px→0, opacity 0→1, duration 300ms ease-out) e aplicarstyle={{ animationDelay: \${index * 40}ms` }}nos cards mapeados emfrontend/src/pages/PropertiesPage.tsxpara stagger; resetar animação ao trocar de página (chave nokey` 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-motionoverride (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— calcularfrom = (page - 1) * perPage + 1,to = Math.min(page * perPage, total); renderizar próximo ao contador de resultados ou acima da paginação inferiorCrité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 quandoscrollY > 400viauseEffectcom listener descroll, chamawindow.scrollTo({ top: 0, behavior: 'smooth' })ao clicar; integrar emfrontend/src/pages/PropertiesPage.tsxcomo filho direto da páginaCrité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.tsxefrontend/src/components/PropertyGridCard.tsx— badge "Destaque" quandoproperty.is_featured === true(fundo âmbar,⭐ Destaque); badge "Novo" quandocreated_atfor de até 7 dias atrás — calculado no frontend:Date.now() - new Date(created_at).getTime() < 7 * 24 * 60 * 60 * 1000; posicionarabsolute top-2 left-2na div da fotoCritérios de aceitação:
- Imóvel com
is_featured=trueexibe badge "⭐ Destaque" na foto - Imóvel com
created_atde ontem exibe badge "Novo" na foto - Imóvel com
created_atde 8 dias atrás não exibe badge "Novo" - Ambos os badges podem coexistir no mesmo card
- Imóvel com
-
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, adicionaronKeyDownque responde aArrowLeft(prev) eArrowRight(next);aria-label="Foto anterior"/"Próxima foto"nos botõesCrité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, comaria-label="Paginação superior"; visível apenas quandoresult.pages > 1Crité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 enquantocatalogLoading === true; a listagem de imóveis continua carregando independentementeCritérios de aceitação:
- Enquanto
catalogLoadingfor 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
- Enquanto
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
/imoveisno browser e verificar ausência de<button>dentro de<a>em todos os cards (lista e grade) — corrigir qualquer instância remanescente emfrontend/src/components/PropertyRowCard.tsxoufrontend/src/components/PropertyGridCard.tsxCritérios de aceitação:
- DevTools → Elements: nenhum seletor
a button,a [role=button]encontrado - Validação HTML5 sem erros de aninhamento inválido
- DevTools → Elements: nenhum seletor
-
T027 Executar testes backend e verificar build TypeScript sem erros —
docker-compose exec backend uv run pytest tests/test_properties.py -vdeve terminar verde;docker-compose exec frontend npx tsc --noEmitdeve terminar sem errosCritérios de aceitação:
- Todos os testes pytest de
test_properties.pypassam - Compilação TypeScript sem erros de tipo
- Nenhum
console.errorno browser ao carregar/imoveis
- Todos os testes pytest de
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
qesort - 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 |