feat: add full project - backend, frontend, docker, specs and configs

This commit is contained in:
MatheusAlves96 2026-04-20 23:59:45 -03:00
parent b77c7d5a01
commit e6cb06255b
24489 changed files with 61341 additions and 36 deletions

View file

@ -0,0 +1,36 @@
# Specification Quality Checklist: Homepage Imersiva com Scroll
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-04-17
**Feature**: [spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- Todos os itens passaram na validação inicial. Sem necessidade de clarificações adicionais.
- Escopo limita intencionalmente o upload de imagens (fora do escopo) — administrador usa URL.
- `prefers-reduced-motion` incluído como FR-015 após identificação no edge case.

View file

@ -0,0 +1,393 @@
# 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 `<HeroSection>` 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/<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")`
```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 `<section>`:
```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 && (
<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
```typescript
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:
```tsx
{/* 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:
```tsx
{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:
```tsx
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:
```tsx
style={{
animation: `fadeDown 1.4s ease-in-out ${i * 0.2}s infinite`,
}}
```
Adicionar no bloco `<style>` existente:
```css
@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>`:
```tsx
// 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_CONFIG` já terá `hero_image_url: null` apó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 `AdminHomepagePage` ainda. O campo `hero_image_url` pode 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-config` em `backend/app/routes/admin.py` que aceita `HomepageConfigIn` e 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)
```json
{
"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
```typescript
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 `onError` na `<img>` para esconder/remover a imagem, deixando o gradiente visível:
> ```tsx
> <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-0` por 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→0` com stagger de 60ms (max 240ms)
- [ ] **US-3**: `ScrollHint` visível sobre o hero com 3 chevrons em cascata
- [ ] **US-4**: Redirecionamento para `/imoveis` após 800ms quando sentinel 100% visível
- [ ] **US-4**: Overlay de transição (blur + spinner) antes de navegar
- [ ] **US-5**: `hero_image_url` salvo 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_url` configurado

View file

@ -0,0 +1,142 @@
# Feature Specification: Homepage Imersiva com Scroll — Hero + Destaque de Imóveis
**Feature Branch**: `018-homepage-scroll-hero`
**Created**: 2026-04-17
**Status**: Draft
**Input**: User description: "Homepage imersiva com scroll — hero + imagem de fundo + destaque de imóveis"
## User Scenarios & Testing *(mandatory)*
### User Story 1 — Visitante visualiza a hero fullscreen ao acessar a homepage (Priority: P1)
Ao entrar na homepage, o visitante vê uma imagem de fundo ocupando 100% da altura da tela com o texto principal (headline, subheadline e CTA) centralizado por cima. Um overlay semitransparente escuro garante que o texto branco seja legível independente da imagem usada.
**Why this priority**: É a primeira impressão do produto. Sem isso, toda a experiência de scroll não existe.
**Independent Test**: Pode ser testado acessando a homepage com e sem `hero_image_url` configurado no painel. Valor entregue: apresentação visual impactante imediata.
**Acceptance Scenarios**:
1. **Given** o administrador configurou `hero_image_url` no HomepageConfig, **When** um visitante acessa a homepage, **Then** a imagem aparece como fundo fullscreen (100vh), com overlay escuro semitransparente e o texto (headline, subheadline, CTA) centralizado sobre ela em cor branca.
2. **Given** `hero_image_url` não está configurado, **When** um visitante acessa a homepage, **Then** uma imagem padrão do sistema é exibida como fundo, mantendo o mesmo layout e legibilidade.
3. **Given** o visitante acessa a homepage em dispositivo móvel, **When** a página carrega, **Then** a imagem de fundo cobre 100% da altura da viewport sem distorção ou corte indesejado.
---
### User Story 2 — Visitante rola a tela e os cards de imóveis emergem sobre o hero (Priority: P1)
Ao começar a rolar a página para baixo, a imagem hero permanece "presa" (sticky) na tela enquanto os cards de imóveis em destaque sobem por cima dela, criando a sensação de que os imóveis emergem da cena. Cada card aparece com animação suave ao entrar no viewport.
**Why this priority**: É a interação central da feature — sem o scroll sticky + cards animados, a experiência imersiva não existe.
**Independent Test**: Pode ser testado rolando a página com imóveis em destaque cadastrados. Valor entregue: diferenciação visual clara da homepage.
**Acceptance Scenarios**:
1. **Given** o visitante está na homepage com imóveis em destaque cadastrados, **When** rola a tela para baixo, **Then** a imagem hero permanece visível e fixa enquanto a lista de cards sobe por cima dela.
2. **Given** os cards estão fora do viewport inicial, **When** cada card entra na área visível da tela durante o scroll, **Then** ele aparece com animação de `opacity 0→1` combinada com `translateY(48px→0)`, com stagger de 60ms entre os cards.
3. **Given** não há imóveis em destaque cadastrados, **When** o visitante acessa a homepage, **Then** a seção de cards não é exibida e a experiência de sticky hero ainda funciona normalmente.
---
### User Story 3 — Indicador visual guia o visitante a rolar a tela (Priority: P2)
Enquanto a imagem hero está visível na tela, um indicador de "Role para ver os destaques" aparece com três chevrons animados em cascata, incentivando o visitante a rolar.
**Why this priority**: Melhora a usabilidade e descoberta, mas a feature principal funciona sem ele.
**Independent Test**: Pode ser testado verificando a presença e visibilidade do indicador enquanto o hero está visível, e sua ausência após o scroll. Valor entregue: orientação visual ao usuário.
**Acceptance Scenarios**:
1. **Given** o visitante está na homepage com a imagem hero visível, **When** a página carrega, **Then** aparece a mensagem "Role para ver os destaques" com 3 chevrons animados em cascata (animação encadeada, não simultânea).
2. **Given** o indicador está visível, **When** o visitante rola até a imagem hero sair completamente do viewport, **Then** o indicador desaparece ou some com transição suave.
---
### User Story 4 — Visitante é redirecionado automaticamente para a listagem completa ao fim dos destaques (Priority: P2)
Ao chegar ao fim da lista de imóveis em destaque, o visitante é automaticamente levado para a página de listagem completa (`/imoveis`) após uma breve pausa, com um overlay de transição visual.
**Why this priority**: Cria um fluxo contínuo e natural entre a homepage e o catálogo, mas não bloqueia as funcionalidades principais.
**Independent Test**: Pode ser testado chegando ao fim dos cards e aguardando o redirecionamento. Valor entregue: redução de fricção no funil de aquisição.
**Acceptance Scenarios**:
1. **Given** o visitante rolou até o fim da lista de destaques, **When** o marcador invisível ao fim da lista fica 100% visível no viewport, **Then** após 800ms o visitante é redirecionado para `/imoveis` com overlay de transição (blur + spinner).
2. **Given** o redirecionamento foi acionado, **When** o overlay de transição aparece, **Then** a tela exibe um efeito de blur sobre o conteúdo atual e um spinner indicando carregamento.
3. **Given** o visitante não chegou ao fim da lista, **When** rola para cima novamente, **Then** nenhum redirecionamento é acionado.
---
### User Story 5 — Administrador configura a imagem de fundo do hero (Priority: P3)
O administrador pode definir a URL da imagem de fundo do hero através do painel de configuração da homepage, persistindo a informação no backend.
**Why this priority**: Permite customização, mas a homepage funciona com imagem padrão enquanto não configurada.
**Independent Test**: Pode ser testado salvando uma URL de imagem no painel e verificando que ela aparece na homepage. Valor entregue: controle editorial da vitrine principal.
**Acceptance Scenarios**:
1. **Given** o administrador acessa o painel de configuração da homepage, **When** informa uma URL válida no campo de imagem do hero e salva, **Then** a homepage passa a exibir essa imagem como fundo.
2. **Given** o administrador informa uma URL inválida ou vazia, **When** salva, **Then** o sistema usa a imagem padrão e não exibe erro ao visitante.
3. **Given** a configuração foi salva, **When** a API de configuração da homepage é consultada, **Then** o campo `hero_image_url` aparece na resposta com o valor configurado.
---
### Edge Cases
- O que acontece quando a URL da `hero_image_url` aponta para uma imagem que não carrega (404, timeout)? → A imagem padrão deve ser exibida como fallback.
- O que acontece quando há apenas 1 imóvel em destaque? → A animação e o scroll redirect devem funcionar normalmente.
- O que acontece quando o visitante tem preferência por `prefers-reduced-motion`? → As animações de entrada dos cards e dos chevrons devem ser suprimidas ou substituídas por transições simples de opacidade.
- O que acontece quando `hero_image_url` é uma string vazia (`""`)?→ Tratada como `null`, usando a imagem padrão.
- O que acontece quando o usuário rola rapidamente até o fim antes das animações terminarem? → Os cards já visíveis devem aparecer sem aguardar o stagger completo; o redirecionamento dispara normalmente.
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: O sistema DEVE exibir a seção hero ocupando 100% da altura do viewport (100vh) ao carregar a homepage.
- **FR-002**: O sistema DEVE exibir um overlay escuro semitransparente sobre a imagem do hero para garantir legibilidade do texto branco.
- **FR-003**: O sistema DEVE exibir headline, subheadline e CTA configuráveis (via `HomepageConfig`) centralizados sobre o hero.
- **FR-004**: O sistema DEVE usar `hero_image_url` da configuração como imagem de fundo do hero quando disponível, e uma imagem padrão quando não configurado ou inválido.
- **FR-005**: O sistema DEVE manter a imagem hero "sticky" durante o scroll enquanto os cards de imóveis sobem por cima dela.
- **FR-006**: O sistema DEVE exibir os imóveis em destaque como cards em layout de linha horizontal (`PropertyRowCard`).
- **FR-007**: Cada card de imóvel em destaque DEVE ter animação de entrada (`opacity 0→1` + `translateY 48px→0`) ativada por `IntersectionObserver` com `threshold 0.05` quando o card entra no viewport.
- **FR-008**: O stagger entre animações de cards consecutivos DEVE ser de 60ms.
- **FR-009**: O sistema DEVE exibir um indicador "Role para ver os destaques" com 3 chevrons animados em cascata enquanto o hero estiver visível.
- **FR-010**: O sistema DEVE ocultar o indicador de scroll quando a imagem hero sair do viewport.
- **FR-011**: O sistema DEVE redirecionar automaticamente para `/imoveis` após 800ms quando um marcador invisível ao fim da lista de cards estiver 100% visível no viewport (`IntersectionObserver`, `threshold 1.0`).
- **FR-012**: O redirecionamento DEVE ser acompanhado por um overlay de transição (blur + spinner) antes de navegar.
- **FR-013**: A tabela `homepage_config` no banco de dados DEVE receber a nova coluna `hero_image_url` (texto de até 512 caracteres, opcional/nullable) via migration Alembic.
- **FR-014**: A API `GET /homepage-config` DEVE incluir o campo `hero_image_url` na resposta.
- **FR-015**: O sistema DEVE respeitar `prefers-reduced-motion` suprimindo ou simplificando animações de entrada para usuários que assim preferirem.
### Key Entities
- **HomepageConfig**: Registro único de configuração da homepage. Passa a incluir `hero_image_url` (URL da imagem de fundo do hero, opcional). Demais campos existentes (headline, subheadline, CTA) permanecem inalterados.
- **FeaturedProperty** (imóvel em destaque): Imóvel marcado para aparecer na seção de destaques da homepage. Sem alteração de modelo; apenas o modo de exibição muda.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: O tempo de carregamento inicial da homepage (até a imagem hero ser visível) não ultrapassa 3 segundos em conexões padrão.
- **SC-002**: 100% dos imóveis em destaque exibem animação de entrada ao entrar no viewport durante o scroll.
- **SC-003**: O redirecionamento automático para `/imoveis` ocorre dentro de 800ms (±100ms) a partir do momento em que o marcador final é 100% visível.
- **SC-004**: A imagem padrão é exibida corretamente em 100% dos acessos onde `hero_image_url` não está configurado ou não carrega.
- **SC-005**: A experiência de scroll sticky funciona corretamente nos navegadores modernos (Chrome, Firefox, Safari, Edge — versões dos últimos 2 anos).
- **SC-006**: Em dispositivos com `prefers-reduced-motion` ativado, nenhuma animação de movimento (translateY, chevron cascade) é executada.
## Assumptions
- O componente `HomeScrollScene.tsx` já existe no codebase mas ainda sem integração completa com o hero text — a spec assume que ele será refatorado para receber todo o conteúdo (hero + lista) como uma única unidade.
- O `PropertyRowCard` já existe e está funcional; a spec não envolve alteração em seu layout interno.
- A imagem padrão do sistema está disponível como asset estático no frontend.
- O painel de administração já possui interface para editar configurações da `HomepageConfig`; a adição do campo `hero_image_url` segue o mesmo padrão já existente.
- O endpoint `GET /homepage-config` já existe; a mudança é apenas aditiva (novo campo na resposta).
- Os imóveis em destaque já são retornados por uma rota existente; não é necessário criar um novo endpoint.
- A administração de `hero_image_url` é feita informando uma URL externa ou um path de asset já hospedado (upload de imagem está fora do escopo desta feature).
- A migration Alembic será executada como parte do processo de deploy padrão do projeto.

View file

@ -0,0 +1,171 @@
# Tasks: Feature 018 — Homepage Imersiva com Scroll (Hero + Destaques)
**Feature Branch**: `018-homepage-scroll-hero`
**Input**: `.specify/features/018-homepage-scroll-hero/plan.md`, `.specify/features/018-homepage-scroll-hero/spec.md`
**Prerequisites**: plan.md ✅, spec.md ✅
## Format: `[ID] [P?] [Story?] Description — caminho`
- **[P]**: Pode rodar em paralelo (arquivos diferentes, sem dependências de tarefas incompletas)
- **[Story]**: User story correspondente (US1US5, mapeado ao spec.md)
---
## Phase 1: Foundational — Backend + Frontend Types (Pré-requisitos bloqueantes)
**Purpose**: Expor `hero_image_url` no banco de dados, no backend e no frontend. Todas as fases de user story dependem desta fase.
**⚠️ CRÍTICO**: Nenhuma user story pode ser iniciada até esta fase estar completa.
- [ ] T001 Criar migration Alembic com `revision = "d1e2f3a4b5c6"`, `down_revision = "c8d9e0f1a2b3"`: `upgrade()` executa `op.add_column("homepage_config", sa.Column("hero_image_url", sa.String(512), nullable=True))`, `downgrade()` executa `op.drop_column``backend/migrations/versions/d1e2f3a4b5c6_add_hero_image_url_to_homepage_config.py`
- [ ] T002 [P] Adicionar campo `hero_image_url = db.Column(db.String(512), nullable=True)` ao modelo `HomepageConfig` após `featured_properties_limit``backend/app/models/homepage.py`
- [ ] T003 [P] Adicionar `hero_image_url: str | None = None` a `HomepageConfigOut` e `HomepageConfigIn`; em `HomepageConfigIn` incluir `@field_validator("hero_image_url", mode="before")` que converte string vazia ou de apenas espaços para `None``backend/app/schemas/homepage.py`
- [ ] T004 [P] Adicionar campo `hero_image_url?: string | null` à interface `HomepageConfig`; adicionar `hero_image_url: null` ao objeto `FALLBACK_CONFIG``frontend/src/types/homepage.ts` e `frontend/src/pages/HomePage.tsx`
**Checkpoint**: `GET /api/v1/homepage-config` retorna `hero_image_url`; interface TypeScript corretamente tipada.
---
## Phase 2: User Story 1 — Visitante visualiza a hero fullscreen (P1) 🎯 MVP
**Goal**: Hero ocupa 100vh com imagem de fundo opcional, overlay escuro semitransparente e headline/subheadline/CTA centralizados; fallback para gradiente CSS quando sem imagem.
**Independent Test**: Acessar homepage com `hero_image_url` configurado e verificar imagem fullscreen com overlay escuro e texto branco legível. Acessar sem configuração e verificar exibição do gradiente padrão com texto igualmente legível.
- [ ] T005 [P] [US1] Adicionar prop `backgroundImage?: string | null` à interface `HeroSectionProps`; quando `backgroundImage` presente, aplicar `style={{ backgroundImage: \`url(\${backgroundImage})\`, backgroundSize: 'cover', backgroundPosition: 'center' }}` ao `<section>` (substituindo o gradiente); adicionar `<div className="absolute inset-0 pointer-events-none" style={{ background: 'rgba(0,0,0,0.52)' }} aria-hidden="true" />` dentro da section; garantir conteúdo com `position: relative; z-index: 1` — `frontend/src/components/HeroSection.tsx`
- [ ] T006 [P] [US1] Atualizar `HomeScrollScene.tsx`: (a) expandir interface `HomeScrollSceneProps` para incluir `headline: string`, `subheadline?: string | null`, `ctaLabel?: string`, `ctaUrl?: string`, `backgroundImage?: string | null`, `isLoading?: boolean`; (b) renomear prop `imageUrl``backgroundImage` em toda a implementação; (c) adicionar `<div className="absolute inset-0 pointer-events-none" style={{ background: 'rgba(0,0,0,0.50)' }} aria-hidden="true" />` após o bloco `{backgroundImage ? <img> : <div gradiente>}` e antes do overlay de gradiente existente — apenas quando `backgroundImage` presente; (d) adicionar bloco de hero text com skeleton de loading e conteúdo real (h1, p, `<a>`) dentro do container sticky, posicionado com `className="absolute inset-0 flex flex-col items-center justify-center px-6 text-center z-10 pt-14"``frontend/src/components/HomeScrollScene.tsx`
- [ ] T007 [US1] Remover import e uso de `<HeroSection>` do JSX de `HomePage`; passar ao `<HomeScrollScene>` as props: `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}``frontend/src/pages/HomePage.tsx`
**Checkpoint**: Hero fullscreen com imagem de fundo (ou gradiente como fallback), overlay escuro e texto centralizado visível e legível.
---
## Phase 3: User Story 2 — Cards de imóveis emergem durante o scroll (P1)
**Goal**: Imagem hero permanece sticky enquanto cards sobem por cima com animação `opacity 0→1` + `translateY 48px→0`; stagger 60ms entre cards; usuários com `prefers-reduced-motion` não recebem animações de movimento.
**Independent Test**: Rolar a homepage com imóveis em destaque cadastrados; verificar sticky hero + animação de entrada por card com stagger. Ativar `prefers-reduced-motion: reduce` no DevTools e verificar ausência de `translateY`.
> **Nota**: Animação `RiseCard` (IntersectionObserver `threshold: 0.05`, stagger via `transitionDelay: ${index * 60}ms`, container sticky `h-screen z-0`) já está implementada. Esta fase adiciona apenas suporte a `prefers-reduced-motion`.
- [ ] T008 [US2] Adicionar classes `motion-reduce:transition-none motion-reduce:translate-y-0` ao `className` do `<div>` animado dentro de `RiseCard` para suprimir a transição CSS e o `translateY` quando `prefers-reduced-motion` estiver ativo — `frontend/src/components/HomeScrollScene.tsx`
**Checkpoint**: Cards animam com opacity + translateY normalmente; `prefers-reduced-motion` suprime o movimento mantendo a visibilidade.
---
## Phase 4: User Story 3 — Indicador visual guia o visitante a rolar (P2)
**Goal**: `ScrollHint` exibe "Role para ver os destaques" com 3 chevrons animados em cascata enquanto o hero é visível; animação suprimida em `prefers-reduced-motion`.
**Independent Test**: Verificar label "Role para ver os destaques" e 3 chevrons animados enquanto hero visível; ativar `prefers-reduced-motion: reduce` e verificar que chevrons param de animar.
> **Nota**: `ScrollHint` com 3 chevrons animados em cascata (keyframe `fadeDown`, `animation-delay: ${i * 0.2}s`) já existe. Esta fase atualiza o label e adiciona suporte a `prefers-reduced-motion`.
- [ ] T009 [US3] Em `HomeScrollScene.tsx`: alterar o label fixo passado ao `<ScrollHint>` de `"Imóveis em destaque"` para `"Role para ver os destaques"`; adicionar `className="scroll-chevron"` em cada `<svg>` dos chevrons dentro de `ScrollHint`; acrescentar ao bloco `<style>` inline a regra `@media (prefers-reduced-motion: reduce) { .scroll-chevron { animation: none !important; } }``frontend/src/components/HomeScrollScene.tsx`
**Checkpoint**: Indicador exibe texto correto; chevrons animam normalmente e têm animação suprimida em `prefers-reduced-motion`.
---
## Phase 5: User Story 4 — Redirecionamento automático ao fim dos destaques (P2)
**Goal**: Após 800ms com marcador final 100% visível, redirecionar para `/imoveis` com overlay blur + spinner.
**Independent Test**: Rolar até o fim dos cards de destaque; aguardar 800ms; verificar overlay com blur e spinner e redirect para `/imoveis`.
> **Nota**: Implementação completa já existe em `HomeScrollScene.tsx`: `sentinelRef` + `IntersectionObserver threshold: 1.0` + `setTimeout 800ms` + estado `navigating` + overlay com `backdropFilter: blur(8px)` + spinner animado + `navigate('/imoveis')`. Esta fase apenas melhora a acessibilidade do overlay.
- [ ] T010 [US4] Adicionar `role="status"` e `aria-live="polite"` ao `<div>` do overlay de transição (bloco `{navigating && (...)}`), e `aria-label="Redirecionando para a listagem de imóveis"` ao spinner, para acessibilidade de screen readers — `frontend/src/components/HomeScrollScene.tsx`
**Checkpoint**: Redirect automático funcionando em 800ms com overlay blur+spinner e overlay acessível via `aria-live`.
---
## Phase 6: User Story 5 — Administrador configura a imagem de fundo do hero (P3)
**Goal**: Admin persiste URL da imagem via painel; API retorna o valor; homepage exibe a imagem configurada.
**Independent Test**: Salvar URL válida via admin panel; consultar `GET /api/v1/homepage-config` e verificar `hero_image_url` na resposta; confirmar exibição na homepage. Salvar string vazia e verificar que API retorna `null` e fallback de gradiente é usado.
> **Nota**: A spec assume que o painel admin para `HomepageConfig` já existe, mas nenhum endpoint `PUT` nem página admin foram encontrados no codebase. Esta fase cria ambos. O schema `HomepageConfigIn` (atualizado em T003) já valida o campo.
- [ ] T011 [P] [US5] Adicionar endpoint `PUT /api/v1/admin/homepage-config` ao blueprint `homepage_bp`: requer JWT de admin (usar decorator de auth já existente em `backend/app/utils/auth.py`), valida payload com `HomepageConfigIn`, atualiza registro via SQLAlchemy (`.query.first()` + atribuição de atributos + `db.session.commit()`), retorna `HomepageConfigOut.model_validate(config).model_dump()``backend/app/routes/homepage.py`
- [ ] T012 [US5] Criar `AdminHomepagePage.tsx` com formulário controlado (campos: `hero_headline`, `hero_subheadline`, `hero_cta_label`, `hero_cta_url`, `featured_properties_limit`, `hero_image_url`); buscar config atual com `GET /api/v1/homepage-config` ao montar; salvar com `PUT /api/v1/admin/homepage-config`; seguir padrão visual e de autenticação das demais páginas admin existentes — `frontend/src/pages/admin/AdminHomepagePage.tsx`
**Checkpoint**: Admin salva `hero_image_url` via painel; valor retornado pela API; homepage exibe imagem configurada.
---
## Phase 7: Polish & Cross-Cutting Concerns
**Purpose**: Comportamentos de borda e qualidade transversal.
- [ ] T013 [P] Verificar fallback de imagem padrão: quando `backgroundImage` é `null`, `undefined` ou URL que resulta em erro (add `onError` handler no `<img>` de `HomeScrollScene.tsx` para setar `backgroundImage` state como `null` em caso de falha de carregamento) — `frontend/src/components/HomeScrollScene.tsx`
- [ ] T014 [P] Verificar comportamento mobile: testar hero em viewports < 640px no Chrome DevTools garantindo `backgroundSize: cover` sem distorção; confirmar que hero text e CTA são legíveis em telas pequenas `frontend/src/components/HomeScrollScene.tsx`
---
## Dependencies & Execution Order
### Phase Dependencies
- **Foundational (Phase 1)**: Sem dependências — pode começar imediatamente
- **User Stories (Phases 26)**: Todas dependem da conclusão da Phase 1
- US1 (Phase 2) e US2 (Phase 3): US2 depende de T006 estar completo; demais são independentes entre si
- US3 (Phase 4), US4 (Phase 5), US5 (Phase 6): podem rodar em paralelo entre si após Phase 1
- **Polish (Phase 7)**: Após todas as user stories desejadas estarem completas
### User Story Dependencies
- **US1 (Phase 2, P1)**: Depende da Foundational (T001T004)
- **US2 (Phase 3, P1)**: Depende de T006 (props interface de HomeScrollScene já atualizada)
- **US3 (Phase 4, P2)**: Depende apenas de Phase 1 — independente de US1/US2
- **US4 (Phase 5, P2)**: Já implementado — apenas acessibilidade; depende de Phase 1
- **US5 (Phase 6, P3)**: Depende de T002 e T003 (model e schema já prontos)
### Parallel Execution
```
Phase 1 (Foundational):
T001 → [T002 ∥ T003 ∥ T004]
Phase 2 (US1):
[T005 ∥ T006] → T007
Após Phase 2 (US2US5 em paralelo):
T008 (US2) ∥ T009 (US3) ∥ T010 (US4) ∥ [T011 → T012] (US5)
Phase 7 (Polish):
T013 ∥ T014
```
---
## Implementation Strategy
**MVP (entrega mínima — 7 tarefas)**: Phases 1 + 2 (T001T007)
- Homepage já exibe hero imersivo fullscreen com imagem de fundo opcional, overlay escuro e hero text centralizado
- Sticky scroll + cards animados continuam funcionando (já implementados)
**Incremental após MVP**:
1. T008 (US2) — reduced-motion support para cards
2. T009 (US3) — label correto + reduced-motion no ScrollHint
3. T010 (US4) — acessibilidade no overlay de redirect (núcleo já funcional)
4. T011T012 (US5) — admin panel para configurar `hero_image_url`
5. T013T014 (Polish) — fallback de imagem + validação mobile
---
## Summary
| Fase | User Story | Tarefas | Prioridade |
|------|-----------|---------|-----------|
| Foundational | — | T001T004 | Bloqueante |
| Phase 2 | US1 — Hero Fullscreen | T005T007 | P1 (MVP) |
| Phase 3 | US2 — Cards emergem | T008 | P1 |
| Phase 4 | US3 — Indicador scroll | T009 | P2 |
| Phase 5 | US4 — Auto-redirect | T010 | P2 |
| Phase 6 | US5 — Admin config | T011T012 | P3 |
| Phase 7 | Polish | T013T014 | — |
| **Total** | | **14 tarefas** | |