Add live show player playback and effects
This commit is contained in:
69
resources/js/guest/components/LiveShowBackdrop.tsx
Normal file
69
resources/js/guest/components/LiveShowBackdrop.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import type { LiveShowBackgroundMode, LiveShowPhoto } from '../services/liveShowApi';
|
||||
|
||||
function resolvePhotoUrl(photo?: LiveShowPhoto | null): string | null {
|
||||
if (!photo) {
|
||||
return null;
|
||||
}
|
||||
return photo.full_url || photo.thumb_url || null;
|
||||
}
|
||||
|
||||
function resolveBlurAmount(intensity: number): number {
|
||||
const safe = Number.isFinite(intensity) ? intensity : 70;
|
||||
return 28 + Math.min(60, Math.max(0, safe)) * 0.45;
|
||||
}
|
||||
|
||||
export default function LiveShowBackdrop({
|
||||
mode,
|
||||
photo,
|
||||
intensity,
|
||||
}: {
|
||||
mode: LiveShowBackgroundMode;
|
||||
photo?: LiveShowPhoto | null;
|
||||
intensity: number;
|
||||
}) {
|
||||
const photoUrl = resolvePhotoUrl(photo);
|
||||
const blurAmount = resolveBlurAmount(intensity);
|
||||
const fallbackMode = mode === 'blur_last' && !photoUrl ? 'gradient' : mode;
|
||||
|
||||
if (fallbackMode === 'solid') {
|
||||
return (
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 z-0"
|
||||
style={{ backgroundColor: 'rgb(8, 10, 16)' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (fallbackMode === 'gradient') {
|
||||
return <div className="pointer-events-none absolute inset-0 z-0 bg-aurora-enhanced" />;
|
||||
}
|
||||
|
||||
if (fallbackMode === 'brand') {
|
||||
return (
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 z-0"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'radial-gradient(120% 120% at 15% 15%, var(--guest-primary) 0%, rgba(0,0,0,0.8) 55%), radial-gradient(120% 120% at 85% 20%, var(--guest-secondary) 0%, transparent 60%)',
|
||||
backgroundColor: 'rgb(5, 8, 16)',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-0 z-0">
|
||||
<div
|
||||
className="absolute inset-0 scale-110"
|
||||
style={{
|
||||
backgroundImage: photoUrl ? `url(${photoUrl})` : undefined,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
filter: `blur(${blurAmount}px) saturate(1.15)`,
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/60" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
80
resources/js/guest/components/LiveShowStage.tsx
Normal file
80
resources/js/guest/components/LiveShowStage.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import type { LiveShowLayoutMode, LiveShowPhoto } from '../services/liveShowApi';
|
||||
|
||||
const BASE_TILE =
|
||||
'relative overflow-hidden rounded-[28px] bg-black/70 shadow-[0_24px_70px_rgba(0,0,0,0.55)]';
|
||||
|
||||
function PhotoTile({
|
||||
photo,
|
||||
fit,
|
||||
label,
|
||||
className = '',
|
||||
}: {
|
||||
photo: LiveShowPhoto;
|
||||
fit: 'cover' | 'contain';
|
||||
label: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const src = photo.full_url || photo.thumb_url || '';
|
||||
return (
|
||||
<div className={`${BASE_TILE} ${className}`.trim()}>
|
||||
{src ? (
|
||||
<img
|
||||
src={src}
|
||||
alt={label}
|
||||
className={`h-full w-full object-${fit} object-center`}
|
||||
loading="eager"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center text-sm text-white/60">
|
||||
{label}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function LiveShowStage({
|
||||
layout,
|
||||
photos,
|
||||
title,
|
||||
}: {
|
||||
layout: LiveShowLayoutMode;
|
||||
photos: LiveShowPhoto[];
|
||||
title: string;
|
||||
}) {
|
||||
if (photos.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (layout === 'single') {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center px-6 py-12">
|
||||
<PhotoTile
|
||||
photo={photos[0]}
|
||||
fit="contain"
|
||||
label={title}
|
||||
className="h-[62vh] w-full max-w-[1200px] sm:h-[72vh]"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (layout === 'split') {
|
||||
return (
|
||||
<div className="grid h-[72vh] w-full grid-cols-1 gap-6 px-6 py-12 lg:grid-cols-2">
|
||||
{photos.slice(0, 2).map((photo) => (
|
||||
<PhotoTile key={photo.id} photo={photo} fit="cover" label={title} className="h-full w-full" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid h-[72vh] w-full grid-cols-2 grid-rows-2 gap-5 px-6 py-12">
|
||||
{photos.slice(0, 4).map((photo) => (
|
||||
<PhotoTile key={photo.id} photo={photo} fit="cover" label={title} className="h-full w-full" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user