# Plano de Implementação — Feature 018: Homepage Imersiva com Scroll (Hero + Destaques) ## Objetivo Transformar a homepage em uma experiência imersiva de scroll-sticky: o hero ocupa 100vh com imagem de fundo configurável, overlay escuro e texto centralizado; ao rolar, os cards de imóveis em destaque sobem sobre o hero com animação de entrada. O texto do hero (headline, subheadline, CTA) é movido para dentro do `HomeScrollScene`; a `HeroSection` separada é removida do fluxo principal. ## Escopo - **Backend**: nova coluna `hero_image_url` na tabela `homepage_config`, migration Alembic, exposição no schema/rota - **Frontend — tipos**: adicionar `hero_image_url` a `HomepageConfig` - **Frontend — `HeroSection.tsx`**: adicionar prop `backgroundImage` (mantida para uso isolado/admin futuro) - **Frontend — `HomeScrollScene.tsx`**: aceitar todas as props do hero e renderizar headline/subheadline/CTA dentro do container sticky - **Frontend — `HomePage.tsx`**: remover `` separado; passar todo o config para `HomeScrollScene` - Fallback de imagem padrão quando `hero_image_url` for nulo/vazio/inválido - Suporte a `prefers-reduced-motion` --- ## Tarefas ### 1. Backend — Migration Alembic **Arquivo**: `backend/migrations/versions/_add_hero_image_url_to_homepage_config.py` - Criar migration manual seguindo o padrão existente (ex: `c8d9e0f1a2b3`): - `revision`: novo ID hexadecimal (ex: `d1e2f3a4b5c6`) - `down_revision`: `c8d9e0f1a2b3` (última migration conhecida) - `upgrade()`: `op.add_column("homepage_config", sa.Column("hero_image_url", sa.String(512), nullable=True))` - `downgrade()`: `op.drop_column("homepage_config", "hero_image_url")` ```python # Exemplo de estrutura mínima """add hero_image_url to homepage_config Revision ID: d1e2f3a4b5c6 Revises: c8d9e0f1a2b3 Create Date: 2026-04-17 00:00:00.000000 """ from alembic import op import sqlalchemy as sa revision = "d1e2f3a4b5c6" down_revision = "c8d9e0f1a2b3" branch_labels = None depends_on = None def upgrade(): op.add_column( "homepage_config", sa.Column("hero_image_url", sa.String(length=512), nullable=True), ) def downgrade(): op.drop_column("homepage_config", "hero_image_url") ``` --- ### 2. Backend — Modelo `HomepageConfig` **Arquivo**: `backend/app/models/homepage.py` Adicionar campo após `featured_properties_limit`: ```python hero_image_url = db.Column(db.String(512), nullable=True) ``` --- ### 3. Backend — Schemas Pydantic **Arquivo**: `backend/app/schemas/homepage.py` - Em `HomepageConfigOut`: adicionar `hero_image_url: str | None = None` - Em `HomepageConfigIn`: adicionar `hero_image_url: str | None = None` - Adicionar validator que trata string vazia como `None`: ```python @field_validator("hero_image_url", mode="before") @classmethod def empty_str_to_none(cls, v: str | None) -> str | None: if isinstance(v, str) and not v.strip(): return None return v ``` > A rota `GET /api/v1/homepage-config` já serializa via `HomepageConfigOut.model_dump()` — nenhuma alteração necessária na rota. --- ### 4. Frontend — Tipo `HomepageConfig` **Arquivo**: `frontend/src/types/homepage.ts` Adicionar campo: ```typescript export interface HomepageConfig { hero_headline: string hero_subheadline: string | null hero_cta_label: string hero_cta_url: string featured_properties_limit: number hero_image_url?: string | null // ← novo } ``` Atualizar `FALLBACK_CONFIG` em `HomePage.tsx`: ```typescript const FALLBACK_CONFIG: HomepageConfig = { hero_headline: 'Encontre o imóvel dos seus sonhos', hero_subheadline: 'Os melhores imóveis para comprar ou alugar na sua região', hero_cta_label: 'Ver Imóveis', hero_cta_url: '/imoveis', featured_properties_limit: 6, hero_image_url: null, // ← novo } ``` --- ### 5. Frontend — `HeroSection.tsx` (atualização de interface) **Arquivo**: `frontend/src/components/HeroSection.tsx` Adicionar prop `backgroundImage?: string | null` à interface. No estado loaded, usar `backgroundImage` como `background-image` inline (quando presente) em vez do gradiente CSS, com overlay escuro por cima. ```typescript interface HeroSectionProps { headline: string subheadline: string | null ctaLabel: string ctaUrl: string isLoading?: boolean backgroundImage?: string | null // ← novo } ``` No JSX do estado loaded, substituir o `style` do `
`: ```typescript // Se backgroundImage presente: style={backgroundImage ? { backgroundImage: `url(${backgroundImage})`, backgroundSize: 'cover', backgroundPosition: 'center' } : { background: 'radial-gradient(ellipse 80% 50% at 50% -20%, rgba(94,106,210,0.08) 0%, transparent 60%), #08090a' } } ``` Adicionar overlay escuro semitransparente como div inside a section (antes do conteúdo), quando `backgroundImage` estiver presente: ```tsx {backgroundImage && (