13 KiB
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_urlna tabelahomepage_config, migration Alembic, exposição no schema/rota - Frontend — tipos: adicionar
hero_image_urlaHomepageConfig - Frontend —
HeroSection.tsx: adicionar propbackgroundImage(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<HeroSection>separado; passar todo o config paraHomeScrollScene - Fallback de imagem padrão quando
hero_image_urlfor nulo/vazio/inválido - Suporte a
prefers-reduced-motion
Tarefas
1. Backend — Migration Alembic
Arquivo: backend/migrations/versions/<id>_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")
# 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:
hero_image_url = db.Column(db.String(512), nullable=True)
3. Backend — Schemas Pydantic
Arquivo: backend/app/schemas/homepage.py
- Em
HomepageConfigOut: adicionarhero_image_url: str | None = None - Em
HomepageConfigIn: adicionarhero_image_url: str | None = None- Adicionar validator que trata string vazia como
None:@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
- Adicionar validator que trata string vazia como
A rota
GET /api/v1/homepage-configjá serializa viaHomepageConfigOut.model_dump()— nenhuma alteração necessária na rota.
4. Frontend — Tipo HomepageConfig
Arquivo: frontend/src/types/homepage.ts
Adicionar campo:
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:
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.
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 <section>:
// 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:
{backgroundImage && (
<div
className="absolute inset-0 pointer-events-none"
style={{ background: 'rgba(0,0,0,0.52)' }}
aria-hidden="true"
/>
)}
Garantir que o conteúdo fique com position: relative; z-index: 1 para ficar acima do overlay.
6. Frontend — HomeScrollScene.tsx (principal)
Arquivo: frontend/src/components/HomeScrollScene.tsx
6a. Atualizar interface de props
interface HomeScrollSceneProps {
headline: string
subheadline?: string | null
ctaLabel?: string
ctaUrl?: string
backgroundImage?: string | null
isLoading?: boolean
}
6b. Renderizar hero text dentro do container sticky
O bloco <div className="sticky top-0 h-screen ..."> já existe e contém a imagem/gradiente e <ScrollHint>. Adicionar dentro desse bloco, após a imagem e o overlay de gradiente e antes do <ScrollHint>, o conteúdo do hero:
{/* Hero text — centralizado sobre a imagem */}
<div className="absolute inset-0 flex flex-col items-center justify-center px-6 text-center z-10 pt-14">
{isLoading ? (
<div className="w-full max-w-[800px] animate-pulse">
<div className="h-16 md:h-20 lg:h-24 bg-white/[0.06] rounded-lg w-3/4 mx-auto mb-6" />
<div className="h-6 bg-white/[0.06] rounded w-1/2 mx-auto mb-3" />
<div className="h-6 bg-white/[0.06] rounded w-2/5 mx-auto mb-10" />
<div className="h-11 bg-white/[0.06] rounded w-36 mx-auto" />
</div>
) : (
<div className="w-full max-w-[800px]">
<h1
className="text-[40px] md:text-[48px] lg:text-[72px] font-medium text-white leading-tight tracking-display-xl mb-6"
style={{ fontFeatureSettings: '"cv01", "ss03"', textShadow: '0 2px 16px rgba(0,0,0,0.5)' }}
>
{headline}
</h1>
{subheadline && (
<p className="text-lg md:text-xl text-white/70 font-light leading-relaxed mb-10 max-w-[560px] mx-auto"
style={{ textShadow: '0 1px 8px rgba(0,0,0,0.4)' }}>
{subheadline}
</p>
)}
<a
href={ctaUrl ?? '/imoveis'}
className="inline-flex items-center justify-center px-6 py-3 bg-brand hover:bg-accentHover text-white font-semibold text-sm rounded transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2"
aria-label={ctaLabel}
>
{ctaLabel ?? 'Ver Imóveis'}
</a>
</div>
)}
</div>
6c. Overlay escuro sobre a imagem de fundo
Após o bloco {imageUrl ? <img> : <div gradiente>} e antes do overlay de gradiente existente (que suaviza edges), adicionar overlay semitransparente quando backgroundImage estiver presente:
{backgroundImage && (
<div
className="absolute inset-0 pointer-events-none"
style={{ background: 'rgba(0,0,0,0.50)' }}
aria-hidden="true"
/>
)}
6d. Remover prop imageUrl legada, renomear para backgroundImage
A prop atual é imageUrl?: string. Renomear internamente para backgroundImage para consistência com o resto do plano.
6e. prefers-reduced-motion
No RiseCard, substituir as classes transition-all duration-700 e translate-y-12 com suporte a reduced-motion:
className={`
transition-all duration-700 ease-out
motion-reduce:transition-none motion-reduce:translate-y-0
${visible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-12'}
`}
No ScrollHint, nos chevrons com animation inline, adicionar CSS @media (prefers-reduced-motion: reduce) desabilitando a animação:
style={{
animation: `fadeDown 1.4s ease-in-out ${i * 0.2}s infinite`,
}}
Adicionar no bloco <style> existente:
@media (prefers-reduced-motion: reduce) {
.scroll-chevron { animation: none !important; }
}
E adicionar className="scroll-chevron" nos chevrons do ScrollHint.
7. Frontend — HomePage.tsx (simplificação)
Arquivo: frontend/src/pages/HomePage.tsx
- Remover o import e o uso de
<HeroSection>do retorno JSX - Passar todas as props do hero para
<HomeScrollScene>:
// Antes:
<HeroSection
headline={config.hero_headline}
subheadline={config.hero_subheadline}
ctaLabel={config.hero_cta_label}
ctaUrl={config.hero_cta_url}
isLoading={isLoading}
/>
<HomeScrollScene />
// Depois:
<HomeScrollScene
headline={config.hero_headline}
subheadline={config.hero_subheadline}
ctaLabel={config.hero_cta_label}
ctaUrl={config.hero_cta_url}
backgroundImage={config.hero_image_url ?? null}
isLoading={isLoading}
/>
- Remover import de
HeroSection - O
FALLBACK_CONFIGjá teráhero_image_url: nullapós a tarefa 4
8. Backend — Admin: exposição do campo hero_image_url no painel (opcional/P3)
Fora do escopo crítico desta feature — a spec marca US-5 como P3. O painel admin não possui
AdminHomepagePageainda. O campohero_image_urlpode ser configurado diretamente via endpoint PATCH no banco até que a UI admin seja criada em feature futura.Se desejado nesta feature: adicionar endpoint
PATCH /api/v1/admin/homepage-configembackend/app/routes/admin.pyque aceitaHomepageConfigIne atualiza o registro único da tabela.
Ordem de Execução Recomendada
1. Migration Alembic → backend/migrations/versions/
2. Modelo HomepageConfig → backend/app/models/homepage.py
3. Schemas Pydantic → backend/app/schemas/homepage.py
4. Tipo TS → frontend/src/types/homepage.ts
5. HomeScrollScene → adicionar hero props e renderização
6. HeroSection → adicionar backgroundImage prop
7. HomePage → remover HeroSection, passar props
8. (Opcional) Admin PATCH endpoint
Contratos de Dados
GET /api/v1/homepage-config — response (após migration)
{
"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": "https://cdn.example.com/hero.jpg"
}
HomeScrollScene props
interface HomeScrollSceneProps {
headline: string
subheadline?: string | null
ctaLabel?: string
ctaUrl?: string
backgroundImage?: string | null
isLoading?: boolean
}
Comportamento de Fallback
| Situação | Comportamento |
|---|---|
hero_image_url é null ou não configurado |
Gradiente CSS existente é exibido como fundo |
hero_image_url é string vazia "" |
Tratada como null pelo validator Pydantic |
| Imagem de URL falha ao carregar (404/timeout) | Browser exibe fundo vazio; o gradiente CSS deve ser mantido como background do container (aplicar o gradiente como background do <div> wrapper, a <img> fica por cima com object-fit: cover) |
prefers-reduced-motion ativado |
Animações translateY e chevron cascade são desabilitadas |
Implementação do fallback de imagem quebrada: usar
onErrorna<img>para esconder/remover a imagem, deixando o gradiente visível:<img src={backgroundImage} onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = 'none' }} ... />O gradiente CSS deve estar sempre presente no background do container — a imagem fica posicionada
absolute inset-0por cima.
Critérios de Aceite (ligados às User Stories da spec)
- US-1: Hero ocupa 100vh, overlay escuro garante legibilidade do texto branco, imagem de fundo ou gradiente como fallback
- US-1: Texto (headline, subheadline, CTA) centralizado sobre o fundo
- US-2: Scroll sticky funciona — imagem permanece enquanto cards sobem
- US-2: Cada card tem animação
opacity 0→1+translateY 48px→0com stagger de 60ms (max 240ms) - US-3:
ScrollHintvisível sobre o hero com 3 chevrons em cascata - US-4: Redirecionamento para
/imoveisapós 800ms quando sentinel 100% visível - US-4: Overlay de transição (blur + spinner) antes de navegar
- US-5:
hero_image_urlsalvo no DB e retornado na API após migration - Edge: imagem com URL inválida → fallback para gradiente
- Edge:
prefers-reduced-motion→ sem translateY nem chevron cascade - Edge: FALLBACK_CONFIG funciona sem
hero_image_urlconfigurado