sass-imobiliaria/specs/025-favoritos-locais/plan.md
MatheusAlves96 cf5603243c
Some checks failed
CI/CD → Deploy via SSH / Build & Push Docker Images (push) Successful in 1m0s
CI/CD → Deploy via SSH / Deploy via SSH (push) Successful in 4m35s
CI/CD → Deploy via SSH / Validate HTTPS & Endpoints (push) Failing after 46s
feat: features 025-032 - favoritos, contatos, trabalhe-conosco, area-cliente, navbar, hero-light-dark, performance-homepage
- feat(025): favoritos locais com FavoritesContext, HeartButton, PublicFavoritesPage
- feat(026): central de contatos admin (leads/contatos unificados)
- feat(027): configuração da página de contato via admin
- feat(028): trabalhe conosco - candidaturas com upload e admin
- feat(029): UX área do cliente - visitas, comparação, perfil
- feat(030): navbar UX - menu mobile, ThemeToggle, useFavorites
- feat(031): hero light/dark - imagens separadas por tema, upload, preview, seed
- feat(032): performance homepage - Promise.all parallel fetches, sessionStorage cache,
  preload hero image, loading=lazy nos cards, useInView hook, will-change carrossel,
  keyframes em index.css, AgentsCarousel e HomeScrollScene via props
- fix: light mode HomeScrollScene - gradiente, cores de texto, scroll hint

migrations: g1h2i3j4k5l6 (source em leads), h1i2j3k4l5m6 (contact_config),
            i1j2k3l4m5n6 (job_applications), j2k3l4m5n6o7 (hero theme images)
2026-04-22 22:35:17 -03:00

8.6 KiB

Plan: Feature 025 — Favoritos Locais para Visitantes

Branch: 025-favoritos-locais Spec: specs/025-favoritos-locais/spec.md Status: Ready to implement Backend changes: Nenhum


Visão Geral

Extensão do sistema de favoritos existente (autenticado via API) para suportar visitantes não autenticados via localStorage. Inclui:

  1. FavoritesContext armazena favoritos localmente quando não autenticado
  2. Merge automático ao fazer login (local → servidor)
  3. Página pública /favoritos acessível sem conta
  4. HeartButton permite toggle sem redirecionar para login

Análise do Estado Atual

O que já existe

Arquivo Responsabilidade atual
FavoritesContext.tsx Gerencia favoritos autenticados via API; limpa estado no logout
HeartButton.tsx Redireciona para /login se não autenticado
FavoritesPage.tsx Lista favoritos da conta (/area-do-cliente/favoritos)
AuthContext.tsx login() atualiza user e token; sem callback pós-login
clientArea.ts addFavorite, removeFavorite, getFavorites via API

Problema de dados para a página pública

A getProperty(slug) busca por slug — e a API só expõe propriedades por slug. Como localStorage armazenaria apenas IDs, buscar dados da propriedade exigiria uma chamada extra por imóvel.

Decisão: Armazenar snapshots mínimos junto com o ID. O HeartButton recebe o objeto Property como prop opcional e o contexto o persiste localmente.


Arquitetura de Dados

localStorage

Chave : "local_favorites"
Valor : JSON.stringify(LocalFavoriteEntry[])
// Definido em FavoritesContext.tsx
interface LocalFavoriteEntry {
  id: string;               // property UUID
  title: string;
  slug: string;
  price: string;
  type: 'venda' | 'aluguel';
  photos: Array<{ url: string; alt_text: string }>;
  city: { name: string } | null;
}

favoriteIds (Set) é derivado das entries → lookup O(1) para o HeartButton.


Decisões Técnicas

1. Merge no login — onde colocar?

Opção A: Hook dentro de AuthContext.login() (callback explícito) Opção B: useEffect em FavoritesContext reagindo à mudança isAuthenticated false → true

Escolha: Opção B — sem acoplamento entre contextos; o FavoritesContext já observa isAuthenticated.

Lógica no useEffect([isAuthenticated]):

Se isAuthenticated acabou de virar true:
  1. Carregar local entries do localStorage
  2. Se entries.length > 0:
     a. Carregar favoritos do servidor (getFavorites)
     b. Para cada entry local não presente no servidor → addFavorite(entry.id)
     c. Limpar localStorage["local_favorites"]
  3. Carregar favoritos do servidor normalmente (estado final)

Rollback no merge: Se uma chamada addFavorite falhar, os favoritos locais são preservados e não apagados. O merge é retentado no próximo login.

2. toggle() — assinatura

// Atual (autenticado only):
toggle(propertyId: string): Promise<void>

// Novo (suporta ambos):
toggle(propertyId: string, snapshot?: LocalFavoriteEntry): Promise<void>

Quando não autenticado: atualiza localStorage + estado. Sem chamada API. Quando autenticado: comportamento atual (API + optimistic update). snapshot ignorado.

3. HeartButton — passar o snapshot

Prop property?: Property adicionada. Usada para construir o LocalFavoriteEntry ao toggle local.

Quando property não é passado e o usuário não está autenticado: toggle funciona somente pelo ID (sem snapshot armazenado). O imóvel aparecerá como favorito, mas não exibirá dados na PublicFavoritesPage. Isso é aceitável para backward compatibility.

Remoção do navigate('/login') — usuários não autenticados podem favoritar diretamente.

4. Página pública /favoritos

  • localEntries exposto pelo FavoritesContext
  • Se isAuthenticated: exibe banner de redirecionamento para /area-do-cliente/favoritos
  • Se não autenticado: exibe cards com base nos snapshots locais
  • Estado vazio: link para /imoveis
  • Banner de incentivo ao cadastro (P2): sempre visível quando não autenticado

5. Favoritos ao fazer logout

Decisão: Manter localStorage — o visitante não perde favoritos ao deslogar. O useEffect continua carregando do localStorage quando isAuthenticated = false.


Fluxo de Dados

[Não autenticado]
  Clique HeartButton
       ↓
  toggle(id, snapshot)
       ↓
  localStorage["local_favorites"] updated
  favoriteIds state updated (derivado)
       ↓
  HeartButton muda visual imediatamente

[Login]
  AuthContext.login() → setUser() → isAuthenticated vira true
       ↓
  FavoritesContext useEffect([isAuthenticated]) dispara
       ↓
  Lê localEntries do localStorage
  Se localEntries.length > 0:
    getFavorites() → serverIds
    Para cada entry não em serverIds → addFavorite(entry.id)
    Se todos addFavorite OK → removeItem("local_favorites")
    Se erro → preserva localStorage (sem limpar)
  setFavoriteIds(serverIds + merged)

Arquivos a Criar

frontend/src/pages/PublicFavoritesPage.tsx (novo)

Página pública em /favoritos:

  • Usa useFavorites().localEntries para listar cards
  • useAuth().isAuthenticated para condicionar banner/redirecionamento
  • Cards com foto, título, preço, cidade, link para /imoveis/:slug
  • Botão de remoção em cada card (chama toggle(id))
  • Estado vazio com link para /imoveis
  • Banner de incentivo: link para /cadastro e /login?next=/favoritos

Arquivos a Modificar

frontend/src/contexts/FavoritesContext.tsx

Mudanças:

  1. Adicionar LOCAL_KEY = 'local_favorites'
  2. Adicionar LocalFavoriteEntry interface
  3. Adicionar localEntries: LocalFavoriteEntry[] ao estado
  4. Adicionar readLocal() / writeLocal() helpers
  5. useEffect([isAuthenticated]):
    • Se false: carregar do localStorage → derivar favoriteIds
    • Se true (transition): executar merge → depois carregar do servidor
  6. Atualizar toggle(): sem auth → localStorage; com auth → API (existente)
  7. Expor localEntries no FavoritesContextValue

frontend/src/components/HeartButton.tsx

Mudanças:

  1. Adicionar prop property?: Property
  2. Remover navigate('/login') no path não autenticado
  3. Construir LocalFavoriteEntry a partir de property e passar para toggle()

frontend/src/App.tsx

Mudanças:

  1. Importar PublicFavoritesPage
  2. Adicionar <Route path="/favoritos" element={<PublicFavoritesPage />} /> (rota pública)

frontend/src/components/PropertyRowCard.tsx

Mudanças:

  1. Passar property={property} para <HeartButton />

frontend/src/components/PropertyGridCard.tsx

Mudanças:

  1. Passar property={property} para <HeartButton />

frontend/src/pages/PropertyDetailPage.tsx

Mudanças:

  1. Passar property={property} para <HeartButton />

Arquivos Sem Mudanças

  • AuthContext.tsx — sem acoplamento necessário
  • FavoritesPage.tsx (cliente autenticado) — sem mudanças
  • clientArea.ts — sem mudanças
  • Backend — zero alterações

Tratamento de Edge Cases

Cenário Comportamento
Modo navegação anônima (sem localStorage) try/catch em readLocal(); array vazio como fallback
Imóvel removido do sistema Card exibe dados do snapshot; link para detalhe pode retornar 404 (aceitável)
Merge falha por erro de rede localStorage preservado; retentado no próximo login
50+ favoritos locais Renderização React lista longa; sem paginação (aceitável na v1)
HeartButton sem property prop Toggle funciona; sem snapshot salvo; imóvel não aparece em PublicFavoritesPage
Usuário autenticado acessa /favoritos Banner com link para /area-do-cliente/favoritos; não redireciona automaticamente

Sequência de Implementação

1. FavoritesContext.tsx        ← base de tudo
2. HeartButton.tsx             ← unlock não autenticado
3. PropertyRowCard.tsx         ← passa property ao HeartButton
4. PropertyGridCard.tsx        ← idem
5. PropertyDetailPage.tsx      ← idem
6. PublicFavoritesPage.tsx     ← página pública
7. App.tsx                     ← registrar rota

Validação Manual (Smoke Tests)

  1. Sem login → clicar coração em card → ícone preenchido → reload da página → continua preenchido
  2. Sem login → /favoritos → card aparece com foto e link
  3. Fazer login com 2 favoritos locais → /area-do-cliente/favoritos → imóveis aparecem
  4. Favorito já no servidor antes do login → sem duplicata após merge
  5. Usuário autenticado → /favoritos → banner exibe link para área do cliente