- 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)
255 lines
8.6 KiB
Markdown
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`
|
|
|
|
- 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 `<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
|