211 lines
8.0 KiB
TypeScript
211 lines
8.0 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState, useCallback } from 'react'
|
|
import Image from 'next/image'
|
|
import type { FDVideoBlock as FDVideoBlockProps, Media } from '@/payload-types'
|
|
|
|
const maxWidthClasses: Record<string, string> = {
|
|
default: 'max-w-[1200px]',
|
|
narrow: 'max-w-[900px]',
|
|
wide: 'max-w-[1400px]',
|
|
}
|
|
|
|
// Navy is always dark. White/gray adapt to OS dark mode.
|
|
const bgClasses: Record<string, string> = {
|
|
white: 'bg-white dark:bg-fd-navy',
|
|
navy: 'bg-fd-navy',
|
|
gray: 'bg-fd-surface-alt dark:bg-fd-navy',
|
|
yellow: 'bg-fd-yellow',
|
|
transparent: 'bg-transparent',
|
|
}
|
|
|
|
function extractYouTubeId(url: string): string | null {
|
|
if (!url) return null
|
|
const patterns = [
|
|
/(?:youtube\.com\/watch\?v=)([a-zA-Z0-9_-]{11})/,
|
|
/(?:youtu\.be\/)([a-zA-Z0-9_-]{11})/,
|
|
/(?:youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/,
|
|
/(?:youtube\.com\/v\/)([a-zA-Z0-9_-]{11})/,
|
|
/(?:youtube\.com\/shorts\/)([a-zA-Z0-9_-]{11})/,
|
|
]
|
|
for (const pattern of patterns) {
|
|
const match = url.match(pattern)
|
|
if (match) return match[1]
|
|
}
|
|
return null
|
|
}
|
|
|
|
function extractVimeoId(url: string): string | null {
|
|
if (!url) return null
|
|
const match = url.match(/vimeo\.com\/(?:video\/)?(\d+)/)
|
|
return match ? match[1] : null
|
|
}
|
|
|
|
/* Priority #5: Responsive video radius */
|
|
const videoRadius = 'rounded-[32px] md:rounded-[50px] lg:rounded-[70px]'
|
|
|
|
export const FDVideoBlockComponent: React.FC<FDVideoBlockProps> = ({
|
|
heading,
|
|
description,
|
|
videoSource = 'upload',
|
|
videoFile,
|
|
youtubeUrl,
|
|
vimeoUrl,
|
|
thumbnail,
|
|
aspectRatio = '16/9',
|
|
autoplay = false,
|
|
loop = false,
|
|
maxWidth = 'default',
|
|
sectionBackground = 'white',
|
|
textColor = 'auto',
|
|
anchorId,
|
|
}) => {
|
|
const [isPlaying, setIsPlaying] = useState(false)
|
|
|
|
const isDark = sectionBackground === 'navy'
|
|
|
|
// Manual override takes priority; otherwise auto-adapt with dark: fallback
|
|
const headingColor =
|
|
textColor === 'white' ? 'text-white'
|
|
: textColor === 'navy' ? 'text-fd-navy'
|
|
: isDark
|
|
? 'text-fd-yellow'
|
|
: 'text-fd-navy dark:text-fd-yellow'
|
|
|
|
const bodyColor =
|
|
textColor === 'white' ? 'text-white'
|
|
: textColor === 'navy' ? 'text-fd-navy'
|
|
: isDark
|
|
? 'text-white'
|
|
: 'text-fd-navy dark:text-white'
|
|
|
|
const bgClass = bgClasses[sectionBackground ?? 'white'] || 'bg-white dark:bg-fd-navy'
|
|
const containerClass = maxWidthClasses[maxWidth ?? 'default'] || 'max-w-[1200px]'
|
|
|
|
const videoMedia = videoFile as Media | undefined
|
|
const thumbnailMedia = thumbnail as Media | undefined
|
|
const thumbnailUrl = thumbnailMedia?.url || ''
|
|
const hasThumbnail = Boolean(thumbnailUrl)
|
|
|
|
const youtubeId = youtubeUrl ? extractYouTubeId(youtubeUrl) : null
|
|
const vimeoId = vimeoUrl ? extractVimeoId(vimeoUrl) : null
|
|
|
|
const autoThumbnailUrl =
|
|
!hasThumbnail && youtubeId
|
|
? `https://img.youtube.com/vi/${youtubeId}/maxresdefault.jpg`
|
|
: ''
|
|
|
|
const showThumbnailOverlay =
|
|
!isPlaying && (hasThumbnail || autoThumbnailUrl) && videoSource !== 'upload'
|
|
|
|
const handlePlay = useCallback(() => { setIsPlaying(true) }, [])
|
|
|
|
/* Accessibility: include video title in aria-label */
|
|
const videoTitle = heading || 'Video'
|
|
|
|
const PlayButton = () => (
|
|
<div className="absolute inset-0 flex items-center justify-center bg-black/20 group-hover:bg-black/30 transition-colors">
|
|
<div className="w-20 h-20 md:w-24 md:h-24 rounded-full bg-fd-yellow flex items-center justify-center shadow-xl group-hover:scale-110 transition-transform">
|
|
<svg className="w-8 h-8 md:w-10 md:h-10 text-fd-navy ml-1" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M8 5v14l11-7z" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
)
|
|
|
|
return (
|
|
<section id={anchorId || undefined} className={`relative w-full py-16 md:py-20 lg:py-[99px] ${bgClass} overflow-hidden`}>
|
|
<div className={`relative ${containerClass} mx-auto px-6 md:px-8`}>
|
|
|
|
{(heading || description) && (
|
|
<div className="mb-8 md:mb-12 text-center">
|
|
{heading && (
|
|
/* Priority #6: Was font-joey-medium — all other section headings use font-joey-heavy */
|
|
<h2 className={`font-joey-heavy text-fd-h1 mb-4 ${headingColor}`}>
|
|
{heading}
|
|
</h2>
|
|
)}
|
|
{description && (
|
|
<p className={`font-joey text-fd-body-lg max-w-[800px] mx-auto ${bodyColor}`}>
|
|
{description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Priority #5: Responsive radius instead of fixed rounded-[70px] */}
|
|
<div className={`relative w-full overflow-hidden bg-black transition-all duration-300 ${isPlaying ? 'rounded-none' : videoRadius}`}>
|
|
<div className="relative w-full" style={{ paddingBottom: aspectRatio === '16/10' ? '62.5%' : '56.25%' }}>
|
|
|
|
{videoSource === 'upload' && videoMedia?.url && (
|
|
<>
|
|
<video
|
|
src={videoMedia.url}
|
|
className="absolute inset-0 w-full h-full object-cover"
|
|
controls={!autoplay}
|
|
autoPlay={autoplay || undefined}
|
|
muted={autoplay || undefined}
|
|
loop={loop || undefined}
|
|
playsInline
|
|
poster={thumbnailUrl || undefined}
|
|
>
|
|
Din webbläsare stöder inte videouppspelning.
|
|
</video>
|
|
{!autoplay && hasThumbnail && !isPlaying && (
|
|
<button onClick={handlePlay} className="absolute inset-0 w-full h-full z-10 cursor-pointer group" aria-label={`Spela video: ${videoTitle}`}>
|
|
<Image src={thumbnailUrl} alt={(thumbnailMedia as any)?.alt || 'Video miniatyrböld'} fill className="object-cover" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px" />
|
|
<PlayButton />
|
|
</button>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{videoSource === 'youtube' && youtubeId && (
|
|
<>
|
|
{showThumbnailOverlay ? (
|
|
<button onClick={handlePlay} className="absolute inset-0 w-full h-full z-10 cursor-pointer group" aria-label={`Spela YouTube-video: ${videoTitle}`}>
|
|
<Image src={thumbnailUrl || autoThumbnailUrl} alt={(thumbnailMedia as any)?.alt || 'Video miniatyrbild'} fill className="object-cover" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px" />
|
|
<PlayButton />
|
|
</button>
|
|
) : (
|
|
<iframe
|
|
src={`https://www.youtube.com/embed/${youtubeId}?autoplay=${isPlaying ? 1 : 0}&rel=0`}
|
|
title={videoTitle}
|
|
className="absolute inset-0 w-full h-full"
|
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
allowFullScreen
|
|
loading="lazy"
|
|
style={{ border: 'none' }}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{videoSource === 'vimeo' && vimeoId && (
|
|
<>
|
|
{showThumbnailOverlay ? (
|
|
<button onClick={handlePlay} className="absolute inset-0 w-full h-full z-10 cursor-pointer group" aria-label={`Spela Vimeo-video: ${videoTitle}`}>
|
|
<Image src={thumbnailUrl} alt={(thumbnailMedia as any)?.alt || 'Video miniatyrbild'} fill className="object-cover" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px" />
|
|
<PlayButton />
|
|
</button>
|
|
) : (
|
|
<iframe
|
|
src={`https://player.vimeo.com/video/${vimeoId}?autoplay=${isPlaying ? 1 : 0}`}
|
|
title={videoTitle}
|
|
className="absolute inset-0 w-full h-full"
|
|
allow="autoplay; fullscreen; picture-in-picture"
|
|
allowFullScreen
|
|
loading="lazy"
|
|
style={{ border: 'none' }}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|