# 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; localEntries: LocalFavoriteEntry[]; // novo toggle: (propertyId: string, snapshot?: LocalFavoriteEntry) => Promise; 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 `` 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: ``` - [ ] T007 [P] [US1] Atualizar `frontend/src/components/PropertyCard.tsx` — construir snapshot e passar para `` (mesma lógica de T006) - [ ] T008 [P] [US1] Atualizar `frontend/src/pages/PropertyDetailPage.tsx` — construir snapshot a partir de `property` e passar para `` > 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 , após /politica-de-privacidade: } /> ``` --- ## 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 (T001–T010): visitante favorita imóveis localmente e acessa a página `/favoritos`. Fases 4–5 (T011–T012) entregam as histórias P2 (banner + merge no login). --- ## Contagem de Tasks | Fase | Tasks | User Story | |------|-------|-----------| | Foundational | T001, T002 | — | | US1 | T003–T008 | P1 | | US2 | T009–T010 | P1 | | US3 | T011 | P2 | | US4 | T012 | P2 | | Polish | T013–T014 | — | | **Total** | **14 tasks** | |