feat(frontend): video presentation on property detail page

- VideoPlayer: auto-detects YouTube/Vimeo embed or direct video file
- PhotoCarousel: accepts videoUrl prop, shows video as first slide with play icon thumbnail
- PropertyDetailPage: renders video in carousel (position=carousel) or as standalone section (position=section)
- PriceBox: fix sticky offset for navbar height
- PropertyForm: video URL input with live preview + position selector
- AdminPropertiesPage: include video fields in edit initial data
- types/property: add video_url and video_position to PropertyDetail
- utils/getEmbedUrl: helper to resolve YouTube/Vimeo/direct URLs
This commit is contained in:
MatheusAlves96 2026-04-22 23:57:45 -03:00
parent d363a09f36
commit 2e9f903d06
8 changed files with 252 additions and 35 deletions

View file

@ -0,0 +1,38 @@
export type VideoType = 'youtube' | 'vimeo' | 'direct' | 'unknown'
export interface EmbedResult {
type: VideoType
embedUrl: string | null
}
export function getEmbedUrl(url: string): EmbedResult {
const trimmed = url.trim()
if (!trimmed) return { type: 'unknown', embedUrl: null }
// YouTube — watch?v=, youtu.be/, embed/
const ytWatch = trimmed.match(
/(?:youtube\.com\/watch\?(?:.*&)?v=|youtu\.be\/)([A-Za-z0-9_-]{11})/
)
if (ytWatch) {
return { type: 'youtube', embedUrl: `https://www.youtube.com/embed/${ytWatch[1]}` }
}
if (/youtube\.com\/embed\/[A-Za-z0-9_-]{11}/.test(trimmed)) {
return { type: 'youtube', embedUrl: trimmed }
}
// Vimeo — vimeo.com/ID ou player.vimeo.com/video/ID
const vimeoMatch = trimmed.match(/vimeo\.com\/(?:video\/)?(\d+)/)
if (vimeoMatch) {
return { type: 'vimeo', embedUrl: `https://player.vimeo.com/video/${vimeoMatch[1]}` }
}
if (/player\.vimeo\.com\/video\/\d+/.test(trimmed)) {
return { type: 'vimeo', embedUrl: trimmed }
}
// Arquivo direto de vídeo (.mp4 ou .webm)
if (/\.(mp4|webm)(\?.*)?$/i.test(trimmed)) {
return { type: 'direct', embedUrl: trimmed }
}
return { type: 'unknown', embedUrl: null }
}