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

11 KiB
Raw Blame History

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

    // 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

    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:

    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)

    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:

    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

    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):

    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