sass-imobiliaria/specs/025-favoritos-locais/tasks.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

265 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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** | |