chore: seed sample video data, add spec 033 and update instructions
Some checks failed
CI/CD → Deploy via SSH / Build & Push Docker Images (push) Successful in 1m6s
CI/CD → Deploy via SSH / Deploy via SSH (push) Successful in 4m18s
CI/CD → Deploy via SSH / Validate HTTPS & Endpoints (push) Has been cancelled

This commit is contained in:
MatheusAlves96 2026-04-22 23:57:50 -03:00
parent 2e9f903d06
commit e1a1f71fbd
11 changed files with 1911 additions and 2 deletions

View file

@ -0,0 +1,146 @@
# Contracts: Vídeo de Apresentação do Imóvel (033)
**Gerado por**: /speckit.plan
**Data**: 2026-04-22
---
## Endpoint Afetado: PUT /api/admin/properties/:id
**Autenticação**: JWT admin obrigatório (Bearer token)
### Request Body — Campos adicionados
```json
{
"video_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"video_position": "section"
}
```
| Campo | Tipo | Obrigatório | Valores Aceitos | Default |
|-------|------|-------------|-----------------|---------|
| `video_url` | `string \| null` | Não | Qualquer string válida ou `null` | Campo não enviado = não alterado |
| `video_position` | `string` | Não | `"carousel"` \| `"section"` | `"section"` |
**Comportamento especial**:
- `video_url: ""` (string vazia) → equivalente a `null` (remove o vídeo)
- `video_url` com espaços → sanitizado via `.strip()` antes de persistir
- Omitir `video_url` e `video_position` → campos não são alterados (comportamento PUT parcial existente)
### Response Body — Campos adicionados
```json
{
"id": "uuid",
"title": "...",
"video_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"video_position": "section",
"..."
}
```
---
## Endpoint Afetado: GET /api/properties/:slug
### Response Body — Campos adicionados em `PropertyDetailOut`
```json
{
"id": "uuid",
"slug": "apartamento-centro",
"title": "...",
"video_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"video_position": "section",
"photos": [...],
"..."
}
```
| Campo | Tipo | Nullable | Descrição |
|-------|------|----------|-----------|
| `video_url` | `string \| null` | Sim | URL do vídeo ou `null` se não configurado |
| `video_position` | `"carousel" \| "section"` | Não | Posição de exibição; default `"section"` |
---
## Contrato Frontend — Utilitário `getEmbedUrl`
**Arquivo**: `frontend/src/utils/getEmbedUrl.ts`
```ts
export type VideoType = 'youtube' | 'vimeo' | 'direct' | 'unknown'
export interface EmbedResult {
type: VideoType
embedUrl: string | null
}
export function getEmbedUrl(url: string): EmbedResult
```
### Tabela de transformação
| URL de entrada | `type` | `embedUrl` |
|----------------|--------|-----------|
| `https://www.youtube.com/watch?v=VIDEO_ID` | `youtube` | `https://www.youtube.com/embed/VIDEO_ID` |
| `https://youtu.be/VIDEO_ID` | `youtube` | `https://www.youtube.com/embed/VIDEO_ID` |
| `https://www.youtube.com/embed/VIDEO_ID` | `youtube` | URL original (já é embed) |
| `https://vimeo.com/VIDEO_ID` | `vimeo` | `https://player.vimeo.com/video/VIDEO_ID` |
| `https://player.vimeo.com/video/VIDEO_ID` | `vimeo` | URL original (já é embed) |
| `https://example.com/video.mp4` | `direct` | URL original |
| `https://example.com/video.webm` | `direct` | URL original |
| Qualquer outro formato | `unknown` | `null` |
---
## Contrato Frontend — Componente `VideoPlayer`
**Arquivo**: `frontend/src/components/PropertyDetail/VideoPlayer.tsx`
```ts
interface VideoPlayerProps {
url: string
className?: string
}
```
### Comportamento de renderização
| `type` retornado por `getEmbedUrl` | Renderização |
|------------------------------------|-------------|
| `youtube` \| `vimeo` | `<iframe src={embedUrl} allowFullScreen loading="lazy" />` dentro de wrapper 16:9 |
| `direct` | `<video src={url} controls />` dentro de wrapper 16:9 |
| `unknown` | `null` (nada renderizado) |
### Atributos iframe obrigatórios
```html
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
loading="lazy"
title="Vídeo de apresentação do imóvel"
```
---
## Contrato Frontend — Modificação `PhotoCarousel`
**Arquivo**: `frontend/src/components/PropertyDetail/PhotoCarousel.tsx`
```ts
interface PhotoCarouselProps {
photos: PropertyPhoto[]
videoUrl?: string | null // NOVO — opcional
}
```
### Comportamento quando `videoUrl` está presente
- `totalSlides = photos.length + 1` (slide 0 = vídeo)
- Slide 0: renderiza `<VideoPlayer url={videoUrl} />`
- Slides 1..N: renderizam fotos normalmente
- Contador: `{activeIndex + 1} / {totalSlides}` (ex.: `1 / 5` para o vídeo)
- Thumbnail de vídeo no strip: ícone de play com `aria-label="Ver vídeo"`
### Comportamento quando `videoUrl` é null/undefined
- Comportamento idêntico ao atual (sem alteração)