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)
This commit is contained in:
parent
6ef5a7a17e
commit
cf5603243c
106 changed files with 11927 additions and 1367 deletions
255
specs/025-favoritos-locais/plan.md
Normal file
255
specs/025-favoritos-locais/plan.md
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
# 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue