- 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)
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:
FavoritesContextarmazena favoritos localmente quando não autenticado- Merge automático ao fazer login (local → servidor)
- Página pública
/favoritosacessível sem conta HeartButtonpermite 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
- Lê
localEntriesexposto peloFavoritesContext - 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().localEntriespara listar cards useAuth().isAuthenticatedpara 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
/cadastroe/login?next=/favoritos
Arquivos a Modificar
frontend/src/contexts/FavoritesContext.tsx
Mudanças:
- Adicionar
LOCAL_KEY = 'local_favorites' - Adicionar
LocalFavoriteEntryinterface - Adicionar
localEntries: LocalFavoriteEntry[]ao estado - Adicionar
readLocal()/writeLocal()helpers useEffect([isAuthenticated]):- Se
false: carregar do localStorage → derivarfavoriteIds - Se
true(transition): executar merge → depois carregar do servidor
- Se
- Atualizar
toggle(): sem auth → localStorage; com auth → API (existente) - Expor
localEntriesnoFavoritesContextValue
frontend/src/components/HeartButton.tsx
Mudanças:
- Adicionar prop
property?: Property - Remover
navigate('/login')no path não autenticado - Construir
LocalFavoriteEntrya partir depropertye passar paratoggle()
frontend/src/App.tsx
Mudanças:
- Importar
PublicFavoritesPage - Adicionar
<Route path="/favoritos" element={<PublicFavoritesPage />} />(rota pública)
frontend/src/components/PropertyRowCard.tsx
Mudanças:
- Passar
property={property}para<HeartButton />
frontend/src/components/PropertyGridCard.tsx
Mudanças:
- Passar
property={property}para<HeartButton />
frontend/src/pages/PropertyDetailPage.tsx
Mudanças:
- Passar
property={property}para<HeartButton />
Arquivos Sem Mudanças
AuthContext.tsx— sem acoplamento necessárioFavoritesPage.tsx(cliente autenticado) — sem mudançasclientArea.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)
- Sem login → clicar coração em card → ícone preenchido → reload da página → continua preenchido
- Sem login →
/favoritos→ card aparece com foto e link - Fazer login com 2 favoritos locais →
/area-do-cliente/favoritos→ imóveis aparecem - Favorito já no servidor antes do login → sem duplicata após merge
- Usuário autenticado →
/favoritos→ banner exibe link para área do cliente