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