chore: seed sample video data, add spec 033 and update instructions
This commit is contained in:
parent
2e9f903d06
commit
e1a1f71fbd
11 changed files with 1911 additions and 2 deletions
146
specs/033-video-apresentacao-imovel/contracts/api.md
Normal file
146
specs/033-video-apresentacao-imovel/contracts/api.md
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue