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

255 lines
8.6 KiB
Markdown

# 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<string>) é **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<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