146 lines
4.2 KiB
Markdown
146 lines
4.2 KiB
Markdown
# 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)
|