# 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[]) ``` ```typescript // 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 ```typescript // Atual (autenticado only): toggle(propertyId: string): Promise // Novo (suporta ambos): toggle(propertyId: string, snapshot?: LocalFavoriteEntry): Promise ``` 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ê `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 `} />` (rota pública) ### `frontend/src/components/PropertyRowCard.tsx` Mudanças: 1. Passar `property={property}` para `` ### `frontend/src/components/PropertyGridCard.tsx` Mudanças: 1. Passar `property={property}` para `` ### `frontend/src/pages/PropertyDetailPage.tsx` Mudanças: 1. Passar `property={property}` para `` --- ## 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