feat: features 025-032 - favoritos, contatos, trabalhe-conosco, area-cliente, navbar, hero-light-dark, performance-homepage
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(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:
MatheusAlves96 2026-04-22 22:35:17 -03:00
parent 6ef5a7a17e
commit cf5603243c
106 changed files with 11927 additions and 1367 deletions

View file

@ -0,0 +1,36 @@
# Specification Quality Checklist: Favoritos Locais para Visitantes Não Autenticados
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-04-21
**Feature**: [spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- Spec aprovado na primeira validação — todos os itens passam.
- A page `/favoritos` (pública) e `/area-do-cliente/favoritos` (autenticada) são destinos distintos, conforme explicitado nas Assumptions.
- Sincronização de favoritos locais ao login está coberta pela US4 e FR-013 a FR-017.

View 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

View file

@ -0,0 +1,166 @@
# Feature Specification: Favoritos Locais para Visitantes Não Autenticados
**Feature Branch**: `025-favoritos-locais`
**Created**: 2026-04-21
**Status**: Draft
---
## Contexto
O sistema já oferece uma lista de favoritos para usuários autenticados, acessível em `/area-do-cliente/favoritos`. No entanto, visitantes sem cadastro não podem salvar imóveis de interesse durante a navegação — qualquer imóvel marcado como favorito é esquecido ao trocar de página ou fechar o browser.
Este spec cobre a adição de uma experiência de favoritos para visitantes não autenticados, inteiramente do lado do cliente, sem necessidade de cadastro imediato. Inclui também uma página pública `/favoritos` acessível a qualquer visitante e a sincronização automática dos favoritos locais com a conta do servidor quando o visitante decide se cadastrar ou fazer login.
---
## User Scenarios & Testing
### User Story 1 — Visitante Favorita Imóvel Sem Se Cadastrar (Priority: P1)
Um visitante sem cadastro que encontrou um imóvel interessante na listagem ou na página de detalhes quer marcá-lo como favorito para revisitar depois — sem precisar criar uma conta no momento.
**Why this priority**: Exigir cadastro para salvar favoritos cria uma barreira de entrada que reduz o engajamento de visitantes em fase de descoberta. Permitir favoritos locais elimina essa fricção e aumenta o tempo médio de sessão e a probabilidade de conversão posterior.
**Independent Test**: Sem estar logado, clicar no ícone de coração de um card de imóvel na listagem `/imoveis`, verificar que o ícone muda visualmente para preenchido, navegar para outra página e retornar — o imóvel deve continuar marcado como favorito.
**Acceptance Scenarios**:
1. **Given** um visitante não autenticado na página `/imoveis`, **When** ele clica no ícone de favorito de um card, **Then** o ícone muda para o estado "favoritado" (coração preenchido) instantaneamente e o imóvel é salvo localmente.
2. **Given** um visitante não autenticado na página de detalhes de um imóvel, **When** ele clica no botão de favorito, **Then** o ícone muda para o estado "favoritado" e o imóvel é salvo localmente.
3. **Given** um imóvel já favoritado localmente, **When** o visitante clica novamente no ícone de favorito, **Then** o imóvel é removido dos favoritos locais e o ícone volta ao estado vazio.
4. **Given** um imóvel favoritado por um visitante não autenticado, **When** o visitante fecha o browser e reabre o site, **Then** o ícone de favorito daquele imóvel ainda aparece preenchido.
5. **Given** múltiplos imóveis favoritados localmente, **When** o visitante navega pela listagem, **Then** todos os imóveis favoritados exibem o ícone preenchido consistentemente.
---
### User Story 2 — Visitante Acessa a Página Pública de Favoritos (Priority: P1)
Um visitante não autenticado que favoritou imóveis quer ver todos os seus imóveis salvos em uma única página, com informações resumidas e acesso direto à página de detalhes de cada um.
**Why this priority**: Sem uma página dedicada, os favoritos locais têm valor limitado — o usuário não consegue revisitar rapidamente os imóveis que marcou. A página `/favoritos` é o destino principal da funcionalidade e torna a experiência comparável à dos usuários autenticados.
**Independent Test**: Favoritar 3 imóveis como visitante não autenticado, navegar para `/favoritos`, verificar que os 3 imóveis aparecem com foto, título, preço e link para o detalhe.
**Acceptance Scenarios**:
1. **Given** um visitante não autenticado com imóveis favoritados localmente, **When** ele acessa `/favoritos`, **Then** a página exibe todos os imóveis salvos com foto, título, preço e link para a página de detalhes.
2. **Given** a página `/favoritos` com imóveis exibidos, **When** o visitante clica no ícone de remoção de um imóvel, **Then** o imóvel é removido da lista imediatamente, sem recarregar a página.
3. **Given** um visitante não autenticado sem nenhum favorito salvo, **When** ele acessa `/favoritos`, **Then** uma mensagem de estado vazio é exibida com orientação para navegar na listagem e salvar imóveis.
4. **Given** a página `/favoritos` acessível sem autenticação, **When** um visitante não autenticado acessa `/favoritos` diretamente pela URL, **Then** a página carrega normalmente sem redirecionar para login.
5. **Given** a página `/favoritos` com imóveis exibidos, **When** o visitante clica no card de um imóvel, **Then** é redirecionado para a página de detalhes daquele imóvel.
---
### User Story 3 — Banner de Incentivo ao Cadastro na Página de Favoritos (Priority: P2)
Um visitante não autenticado na página `/favoritos` é informado de que pode salvar sua lista de favoritos permanentemente criando uma conta, e recebe um caminho claro para o cadastro.
**Why this priority**: A página `/favoritos` é o momento de maior intenção do visitante — ele demonstrou interesse explícito em imóveis e está revisitando-os. É o ponto de conversão mais natural para incentivar o cadastro.
**Independent Test**: Acessar `/favoritos` sem estar autenticado e verificar que um banner/card de convite ao cadastro é exibido, com botão para criar conta e explicação dos benefícios de sincronização.
**Acceptance Scenarios**:
1. **Given** um visitante não autenticado na página `/favoritos`, **When** a página é carregada, **Then** um banner informativo é exibido explicando que criar uma conta permite salvar os favoritos na nuvem e acessá-los em qualquer dispositivo.
2. **Given** o banner de incentivo visível, **When** o visitante clica em "Criar conta", **Then** é redirecionado para a página de cadastro.
3. **Given** o banner de incentivo visível, **When** o visitante clica em "Entrar", **Then** é redirecionado para a página de login com retorno automático para `/favoritos` após autenticação.
4. **Given** um usuário autenticado na página `/favoritos`, **When** a página é carregada, **Then** o banner de incentivo ao cadastro não é exibido.
---
### User Story 4 — Sincronização de Favoritos Locais ao Fazer Login (Priority: P2)
Um visitante que acumulou favoritos locais e decide se autenticar tem seus favoritos locais automaticamente mesclados com os favoritos já salvos em sua conta, sem precisar refavoritar os imóveis manualmente.
**Why this priority**: Sem sincronização, o usuário perde toda a lista de favoritos locais ao fazer login, o que penaliza exatamente o comportamento desejado (visitar imóveis, favoritar e depois se cadastrar). A perda de dados cria frustração e desincentiva o uso da funcionalidade.
**Independent Test**: Favoritar 3 imóveis sem estar logado (A, B, C). Fazer login em uma conta que já tem o imóvel A nos favoritos do servidor. Navegar para `/area-do-cliente/favoritos` e verificar que A, B e C aparecem — sem duplicatas.
**Acceptance Scenarios**:
1. **Given** um visitante com imóveis A, B e C favoritados localmente, **When** ele faz login com sucesso, **Then** os imóveis B e C (que não estavam nos favoritos do servidor) são adicionados automaticamente à conta do servidor.
2. **Given** que o imóvel A já estava nos favoritos do servidor antes do login, **When** o visitante faz login com A, B e C nos favoritos locais, **Then** o imóvel A não é duplicado nos favoritos do servidor.
3. **Given** que a sincronização foi realizada com sucesso, **When** o login é concluído, **Then** os favoritos locais são removidos do armazenamento local do browser.
4. **Given** um visitante sem nenhum favorito local, **When** ele faz login, **Then** nenhuma operação de merge é realizada e os favoritos do servidor não são alterados.
5. **Given** que a sincronização falha por erro de rede após o login, **When** o visitante acessa a área de favoritos, **Then** os favoritos locais são preservados (não apagados) para retentativa futura.
---
### Edge Cases
- O que acontece com os favoritos locais quando o visitante usa o modo de navegação anônima (aba privada)?
- Como o sistema exibe o estado de carregamento enquanto busca os dados dos imóveis favoritados na página `/favoritos`?
- O que acontece se um imóvel favoritado localmente for removido ou desativado no sistema antes de o visitante acessar `/favoritos`?
- Como o ícone de favorito se comporta em imóveis que aparecem em múltiplos contextos (listagem, detalhe, resultados de busca) simultaneamente na mesma sessão?
- O que acontece com os favoritos locais ao fazer logout — eles são mantidos ou limpos?
- Como o sistema lida com uma lista muito grande de favoritos locais (ex.: 50+ imóveis) em termos de desempenho na página `/favoritos`?
- O que acontece se o visitante tentar acessar `/favoritos` em um browser que não suporta armazenamento local?
---
## Requirements
### Functional Requirements
#### Grupo 1 — Favoritar Sem Autenticação
- **FR-001**: O sistema DEVE permitir que visitantes não autenticados adicionem e removam imóveis de uma lista de favoritos local sem exigir cadastro ou login.
- **FR-002**: O estado de favorito (favoritado ou não) de cada imóvel DEVE ser persistido entre sessões do browser para visitantes não autenticados.
- **FR-003**: O ícone de favorito em cards de listagem e na página de detalhes DEVE refletir o estado local em tempo real para visitantes não autenticados.
- **FR-004**: O toggle de favorito DEVE funcionar de forma idêntica (mesma interface visual e resposta imediata) tanto para usuários autenticados quanto para visitantes não autenticados.
#### Grupo 2 — Página Pública de Favoritos
- **FR-005**: O sistema DEVE disponibilizar uma rota pública `/favoritos` acessível sem autenticação que exiba todos os imóveis favoritados pelo visitante.
- **FR-006**: A página `/favoritos` DEVE buscar os dados atualizados de cada imóvel favoritado pelo seu identificador e exibi-los com foto, título, preço, tipo, número de quartos, área e link para o detalhe.
- **FR-007**: A página `/favoritos` DEVE exibir um estado vazio com orientação de navegação quando o visitante não tiver nenhum favorito salvo.
- **FR-008**: O visitante DEVE poder remover imóveis individualmente da lista na página `/favoritos` sem recarregar a página.
- **FR-009**: A página `/favoritos` NÃO DEVE redirecionar visitantes não autenticados para a página de login.
#### Grupo 3 — Banner de Incentivo ao Cadastro
- **FR-010**: A página `/favoritos` DEVE exibir um banner de incentivo ao cadastro quando o visitante não estiver autenticado, explicando os benefícios de sincronização entre dispositivos.
- **FR-011**: O banner DEVE conter ações claras para cadastro e para login, com retorno automático à página `/favoritos` após autenticação.
- **FR-012**: O banner de incentivo ao cadastro NÃO DEVE ser exibido para usuários autenticados.
#### Grupo 4 — Sincronização no Login
- **FR-013**: Ao concluir o processo de autenticação com sucesso, o sistema DEVE verificar se há favoritos locais armazenados no browser.
- **FR-014**: Caso existam favoritos locais, o sistema DEVE adicionar à conta do servidor apenas os imóveis que ainda não constam nos favoritos do usuário, evitando duplicatas.
- **FR-015**: Após a sincronização bem-sucedida, o sistema DEVE limpar os favoritos locais do browser.
- **FR-016**: Em caso de falha na sincronização, os favoritos locais DEVEM ser preservados para retentativa e o usuário DEVE ser informado discretamente.
- **FR-017**: Usuários que fazem login sem favoritos locais NÃO DEVEM ter os favoritos do servidor alterados.
### Key Entities
- **Favorito Local**: Registro do lado do cliente representando o interesse de um visitante em um imóvel. Identificado pelo `property_id` do imóvel. Não tem representação no banco de dados — existe exclusivamente no armazenamento local do browser do visitante.
- **Imóvel (Property)**: Unidade imobiliária com identificador único, título, preço, tipo, área, quartos e fotos. Consultado pelo identificador na página `/favoritos` para exibir informações atualizadas.
- **Lista de Favoritos Locais**: Conjunto de `property_id`s armazenados localmente pelo visitante, sem limite explícito de tamanho nesta versão.
---
## Success Criteria
### Measurable Outcomes
- **SC-001**: Visitantes não autenticados conseguem favoritar um imóvel em 1 clique, sem fluxo de cadastro ou modal intermediário.
- **SC-002**: O estado de favorito é preservado em 100% das navegações entre páginas e reabertas de browser (sem autenticação).
- **SC-003**: Visitantes conseguem acessar e visualizar todos os seus imóveis favoritados em `/favoritos` em menos de 3 segundos após o carregamento da página.
- **SC-004**: Após o login, 100% dos favoritos locais que não estavam no servidor são sincronizados automaticamente, sem nenhuma ação adicional do usuário.
- **SC-005**: Zero duplicatas de favoritos são criadas no servidor durante o processo de merge ao fazer login.
- **SC-006**: A página `/favoritos` é acessível sem autenticação — 0% de redirecionamentos indesejados para a tela de login para visitantes não autenticados.
- **SC-007**: O banner de incentivo ao cadastro na página `/favoritos` exibe claramente os benefícios e os caminhos para cadastro e login em uma leitura de menos de 10 segundos.
---
## Assumptions
- Os identificadores (`property_id`) dos imóveis são valores estáveis e únicos que não mudam após a criação do imóvel.
- O contexto de autenticação já expõe o estado do usuário (autenticado/não autenticado) de forma acessível aos componentes de favorito existentes.
- A página `/area-do-cliente/favoritos` (para usuários autenticados) permanece inalterada por este spec; a nova página `/favoritos` é um destino separado.
- Imóveis removidos ou desativados no sistema durante o período em que estavam favoritados localmente são tratados exibindo um estado de "imóvel indisponível" na página `/favoritos`, sem remover automaticamente da lista local.
- A lista de favoritos locais NÃO é sincronizada em tempo real entre múltiplos dispositivos do mesmo visitante (sem autenticação não há como identificar o usuário entre dispositivos).
- Favoritos locais são mantidos após o logout — um usuário que se desloga não perde imóveis que eventualmente tenha favoritado antes de se autenticar naquela sessão.
- O backend já possui ou pode receber uma rota para consultar múltiplos imóveis por lista de identificadores, usada pela página `/favoritos` para buscar dados em lote.
- Não há novas tabelas ou alterações no banco de dados nesta feature — toda a persistência para visitantes não autenticados é exclusivamente client-side.

View file

@ -0,0 +1,265 @@
# Tasks: Feature 025 — Favoritos Locais para Visitantes
**Branch**: `025-favoritos-locais`
**Spec**: `specs/025-favoritos-locais/spec.md`
**Plan**: `specs/025-favoritos-locais/plan.md`
**Backend changes**: Nenhum
---
## Fase 1 — Foundational: Interface e Dados (Prerequisito para todos os user stories)
> Objetivo: Definir o contrato de dados `LocalFavoriteEntry` e os utilitários de
> localStorage usados por todo o restante da feature. Nenhuma mudança visível ao usuário.
- [ ] T001 Adicionar interface `LocalFavoriteEntry` e constante `LOCAL_FAV_KEY` em `frontend/src/contexts/FavoritesContext.tsx`
```typescript
// Logo antes de FavoritesContextValue
const LOCAL_FAV_KEY = 'local_favorites';
export interface LocalFavoriteEntry {
id: string;
title: string;
slug: string;
price: string;
type: 'venda' | 'aluguel';
photos: Array<{ url: string; alt_text: string }>;
city: { name: string } | null;
}
```
- [ ] T002 Adicionar `localEntries` ao tipo `FavoritesContextValue` em `frontend/src/contexts/FavoritesContext.tsx`
```typescript
interface FavoritesContextValue {
favoriteIds: Set<string>;
localEntries: LocalFavoriteEntry[]; // novo
toggle: (propertyId: string, snapshot?: LocalFavoriteEntry) => Promise<void>;
isLoading: boolean;
}
```
---
## Fase 2 — User Story 1: Visitante Favorita Imóvel Sem Se Cadastrar (P1)
> **Objetivo**: Ícone de coração funciona para não-autenticados, persiste via localStorage
> e sobrevive a navegação e recarga do browser.
>
> **Teste independente**: Sem login, clicar no coração de um card em `/imoveis`,
> navegar para outra página e retornar — imóvel ainda marcado como favorito.
- [ ] T003 [US1] Refatorar `useEffect([isAuthenticated])` em `frontend/src/contexts/FavoritesContext.tsx` para inicializar `favoriteIds` e `localEntries` a partir do `localStorage` quando não autenticado
**Lógica**:
- Se `!isAuthenticated`: lê `localStorage.getItem(LOCAL_FAV_KEY)`, faz parse para `LocalFavoriteEntry[]`, popula `localEntries` e deriva `favoriteIds` como `new Set(entries.map(e => e.id))`
- Se `isAuthenticated`: comportamento atual (fetch da API); `localEntries` permanece `[]`
- [ ] T004 [US1] Atualizar `toggle()` em `frontend/src/contexts/FavoritesContext.tsx` para aceitar `snapshot?: LocalFavoriteEntry` e tratar o caso não-autenticado via localStorage
**Lógica quando `!isAuthenticated`**:
```typescript
const wasIn = favoriteIds.has(propertyId);
const next = wasIn
? localEntries.filter(e => e.id !== propertyId)
: [...localEntries, snapshot ?? { id: propertyId, title: '', slug: '', price: '', type: 'venda', photos: [], city: null }];
localStorage.setItem(LOCAL_FAV_KEY, JSON.stringify(next));
setLocalEntries(next);
setFavoriteIds(new Set(next.map(e => e.id)));
return;
```
Quando autenticado: comportamento atual; `snapshot` ignorado.
- [ ] T005 [US1] Atualizar `frontend/src/components/HeartButton.tsx` — adicionar prop `snapshot?: LocalFavoriteEntry`, remover redirecionamento para `/login` e chamar `toggle(propertyId, snapshot)`
```typescript
interface HeartButtonProps {
propertyId: string;
snapshot?: LocalFavoriteEntry; // novo
className?: string;
}
// handleClick — remover: if (!isAuthenticated) { navigate('/login'); return; }
// Novo handleClick:
async function handleClick(e: React.MouseEvent) {
e.preventDefault();
e.stopPropagation();
await toggle(propertyId, snapshot);
}
```
Remover import de `useNavigate` e `useAuth` se não forem mais necessários após esta mudança.
- [ ] T006 [P] [US1] Atualizar `frontend/src/components/PropertyRowCard.tsx` — construir snapshot e passar para `<HeartButton>`
A prop `property` já é do tipo `Property`. Compor `LocalFavoriteEntry` a partir de `property`:
```tsx
const favSnapshot: LocalFavoriteEntry = {
id: property.id,
title: property.title,
slug: property.slug,
price: property.price,
type: property.type,
photos: property.photos.slice(0, 1).map(p => ({ url: p.url, alt_text: p.alt_text })),
city: property.city ? { name: property.city.name } : null,
};
// Usar em: <HeartButton propertyId={property.id} snapshot={favSnapshot} />
```
- [ ] T007 [P] [US1] Atualizar `frontend/src/components/PropertyCard.tsx` — construir snapshot e passar para `<HeartButton>` (mesma lógica de T006)
- [ ] T008 [P] [US1] Atualizar `frontend/src/pages/PropertyDetailPage.tsx` — construir snapshot a partir de `property` e passar para `<HeartButton propertyId={property.id} snapshot={favSnapshot} />`
> O objeto `property` completo está disponível no escopo onde `HeartButton` é renderizado (linha 117).
---
## Fase 3 — User Story 2: Página Pública de Favoritos `/favoritos` (P1)
> **Objetivo**: Visitante não autenticado acessa `/favoritos` e vê os imóveis salvos
> localmente com foto, título, preço e link para o detalhe.
>
> **Teste independente**: Favoritar 3 imóveis, acessar `/favoritos` — 3 imóveis exibidos.
- [ ] T009 [US2] Criar `frontend/src/pages/PublicFavoritesPage.tsx`
**Requisitos**:
- Usa `useFavorites()` para ler `localEntries`, `favoriteIds`, `toggle`
- Estado de carregamento: verificar `isLoading`
- Estado vazio: mensagem + link para `/imoveis`
- Grid responsivo: 1 col mobile → 2 col sm → 3 col lg (igual ao `FavoritesPage.tsx` existente)
- Cada card exibe: foto (primeira do array `photos`), título, preço formatado (`Intl.NumberFormat pt-BR`), badge de tipo (`venda`/`aluguel`), cidade, link para `/imoveis/{slug}`, botão de remover (chama `toggle(entry.id)`)
- **Não** chama API — usa apenas `localEntries` do contexto
- Acessível sem autenticação (sem `ProtectedRoute`)
- [ ] T010 [US2] Registrar rota `/favoritos` em `frontend/src/App.tsx`
```tsx
import PublicFavoritesPage from './pages/PublicFavoritesPage';
// Dentro de <Routes>, após /politica-de-privacidade:
<Route path="/favoritos" element={<PublicFavoritesPage />} />
```
---
## Fase 4 — User Story 3: Banner de Incentivo ao Cadastro (P2)
> **Objetivo**: Visitante não autenticado em `/favoritos` vê banner com CTA para
> criar conta ou fazer login.
>
> **Teste independente**: Acessar `/favoritos` sem autenticação e verificar banner com
> botões "Criar conta" e "Entrar".
- [ ] T011 [US3] Adicionar `SignupBanner` inline em `frontend/src/pages/PublicFavoritesPage.tsx`
**Regra**: Banner visível apenas quando `!isAuthenticated` (obter de `useAuth()`).
**Conteúdo**:
- Ícone de coração ou nuvem
- Título: "Salve seus favoritos em qualquer dispositivo"
- Texto: "Crie uma conta gratuita para sincronizar sua lista de imóveis favoritos e acessá-la de qualquer lugar."
- Botão primário: "Criar conta" → navega para `/cadastro`
- Link secundário: "Já tenho conta — Entrar" → navega para `/login?next=/favoritos`
**Posicionamento**: Acima do grid de imóveis (ou abaixo, se a lista for vazia — nesse caso, deve ser o destaque principal da tela).
---
## Fase 5 — User Story 4: Sincronização de Favoritos Locais ao Fazer Login (P2)
> **Objetivo**: Ao fazer login, favoritos locais são mesclados automaticamente com os
> favoritos do servidor, sem duplicatas, e o localStorage é limpo.
>
> **Teste independente**: Favoritar A, B, C sem login; login em conta com A no servidor;
> `/area-do-cliente/favoritos` exibe A, B, C sem duplicatas.
- [ ] T012 [US4] Adicionar `useEffect([isAuthenticated])` de merge em `frontend/src/contexts/FavoritesContext.tsx`
**Lógica** (executada quando `isAuthenticated` muda de `false` para `true`):
```typescript
useEffect(() => {
if (!isAuthenticated) return;
const raw = localStorage.getItem(LOCAL_FAV_KEY);
if (!raw) return; // nada para sincronizar
const localEntries: LocalFavoriteEntry[] = JSON.parse(raw);
if (localEntries.length === 0) return;
(async () => {
try {
const serverFavs = await getFavorites();
const serverIds = new Set(serverFavs.filter((s: any) => s.property_id).map((s: any) => s.property_id as string));
const toAdd = localEntries.filter(e => !serverIds.has(e.id));
await Promise.allSettled(toAdd.map(e => addFavorite(e.id)));
// Limpar localStorage somente se todas as chamadas foram resolvidas (success ou already-exists)
localStorage.removeItem(LOCAL_FAV_KEY);
setLocalEntries([]);
} catch {
// Falha de rede: preservar localStorage para retentativa no próximo login
}
})();
}, [isAuthenticated]);
```
> Este `useEffect` é **separado** do `useEffect` de carregamento inicial (T003).
> Ordem de execução no mesmo ciclo de `isAuthenticated=true`:
> 1. `useEffect` de merge (T012) → adiciona ao servidor e limpa localStorage
> 2. `useEffect` de carregamento (T003) → busca favoritos atualizados do servidor
---
## Fase 6 — Polish & Verificações Finais
- [ ] T013 [P] Verificar `frontend/src/contexts/FavoritesContext.tsx` — confirmar que `localEntries` é exposto no valor do contexto e que o estado é reiniciado corretamente no logout (`isAuthenticated = false` → limpar `localEntries` **do estado**, mas **preservar** o `localStorage["local_favorites"]`)
> Ao fazer logout, os favoritos locais do localStorage são mantidos para que o visitante não os perca caso retorne sem estar logado.
- [ ] T014 [P] Verificar `frontend/src/pages/client/FavoritesPage.tsx` — confirmar que não há quebras de tipo após a alteração da assinatura de `toggle` (o `snapshot` é opcional, não deve impactar chamadas existentes)
---
## Dependências entre Fases
```
T001 → T002 → T003 → T004 ─┬─ T006 (P)
├─ T007 (P)
└─ T008 (P)
T004 → T005 (HeartButton)
T003, T004 → T009 (PublicFavoritesPage)
T009 → T010 (Rota App.tsx)
T009 → T011 (Banner)
T003, T004 → T012 (Merge login)
```
## Execução em Paralelo por Fase
| Fase | Tasks paralelas |
|------|----------------|
| Fase 1 | T001 → T002 (sequencial — mesmo arquivo) |
| Fase 2 | T003 → T004 → T005 (sequencial — mesmo arquivo); T006, T007, T008 em paralelo após T005 |
| Fase 3 | T009 → T010 (sequencial — T010 depende de T009) |
| Fase 4 | T011 (independente de T010, só depende de T009) |
| Fase 5 | T012 (independente das fases 3-4) |
| Fase 6 | T013, T014 em paralelo |
## Escopo MVP (entrega mínima P1)
Fases 1 + 2 + 3 (T001T010): visitante favorita imóveis localmente e acessa a página `/favoritos`.
Fases 45 (T011T012) entregam as histórias P2 (banner + merge no login).
---
## Contagem de Tasks
| Fase | Tasks | User Story |
|------|-------|-----------|
| Foundational | T001, T002 | — |
| US1 | T003T008 | P1 |
| US2 | T009T010 | P1 |
| US3 | T011 | P2 |
| US4 | T012 | P2 |
| Polish | T013T014 | — |
| **Total** | **14 tasks** | |