feat: hero video management — diagonal split, mobile single video, admin editor
- Hero: diagonal 3-video split on desktop, single center video on mobile (CSS breakpoint) - Videos render client-side only to avoid hydration mismatch - Loading overlay (z-5) hides buffering without blocking text content - Admin hero editor: 3 fixed slots (left/center/right) with upload, preview, remove - Center slot marked as main (used on mobile), save blocked until all 3 filled - Upload API: supports MP4/WebM (50MB) in hero folder alongside existing image uploads - Added videos field to hero type and fallback data Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useCallback } from "react";
|
||||
import { useEffect, useRef, useCallback, useState } from "react";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { FloatingHearts } from "@/components/ui/FloatingHearts";
|
||||
import { HeroLogo } from "@/components/ui/HeroLogo";
|
||||
import type { SiteContent } from "@/types/content";
|
||||
|
||||
const VIDEOS = ["/video/ira.mp4", "/video/nadezda.mp4", "/video/nastya-2.mp4"];
|
||||
const DEFAULT_VIDEOS = ["/video/ira.mp4", "/video/nadezda.mp4", "/video/nastya-2.mp4"];
|
||||
|
||||
interface HeroProps {
|
||||
data: SiteContent["hero"];
|
||||
@@ -15,6 +15,21 @@ interface HeroProps {
|
||||
export function Hero({ data: hero }: HeroProps) {
|
||||
const sectionRef = useRef<HTMLElement>(null);
|
||||
const scrolledRef = useRef(false);
|
||||
const overlayRef = useRef<HTMLDivElement>(null);
|
||||
const readyCount = useRef(0);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const videos = hero.videos?.length ? hero.videos : DEFAULT_VIDEOS;
|
||||
const centerVideo = videos[Math.floor(videos.length / 2)] || videos[0];
|
||||
const totalVideos = videos.slice(0, 3).length + 1; // desktop (3) + mobile (1)
|
||||
|
||||
useEffect(() => { setMounted(true); }, []);
|
||||
|
||||
const handleVideoReady = useCallback(() => {
|
||||
readyCount.current += 1;
|
||||
if (readyCount.current >= totalVideos && overlayRef.current) {
|
||||
overlayRef.current.style.opacity = "0";
|
||||
}
|
||||
}, [totalVideos]);
|
||||
|
||||
const scrollToNext = useCallback(() => {
|
||||
const el = sectionRef.current;
|
||||
@@ -64,45 +79,69 @@ export function Hero({ data: hero }: HeroProps) {
|
||||
|
||||
return (
|
||||
<section ref={sectionRef} className="relative flex min-h-svh items-center justify-center overflow-hidden bg-[#050505]">
|
||||
{/* Diagonal split background — 3 dancer videos */}
|
||||
<div className="absolute inset-0">
|
||||
{VIDEOS.map((src, i) => {
|
||||
const positions = [
|
||||
{ left: "0%", width: "38%" },
|
||||
{ left: "31%", width: "38%" },
|
||||
{ left: "62%", width: "38%" },
|
||||
];
|
||||
const clips = [
|
||||
"polygon(0 0, 100% 0, 86% 100%, 0 100%)",
|
||||
"polygon(14% 0, 100% 0, 86% 100%, 0 100%)",
|
||||
"polygon(14% 0, 100% 0, 100% 100%, 0 100%)",
|
||||
];
|
||||
return (
|
||||
<div
|
||||
key={src}
|
||||
className="absolute top-0 bottom-0 overflow-hidden"
|
||||
style={{
|
||||
left: positions[i].left,
|
||||
width: positions[i].width,
|
||||
clipPath: clips[i],
|
||||
}}
|
||||
{/* Videos render only after hydration to avoid SSR mismatch */}
|
||||
{mounted && (
|
||||
<>
|
||||
{/* Mobile: single centered video */}
|
||||
<div className="absolute inset-0 md:hidden">
|
||||
<video
|
||||
autoPlay muted loop playsInline preload="auto"
|
||||
onCanPlayThrough={handleVideoReady}
|
||||
className="absolute inset-0 h-full w-full object-cover object-center"
|
||||
>
|
||||
<video
|
||||
autoPlay muted loop playsInline
|
||||
className="absolute inset-0 h-full w-full object-cover object-center"
|
||||
>
|
||||
<source src={src} type="video/mp4" />
|
||||
</video>
|
||||
<div className="absolute inset-0 bg-black/50" />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* Gold diagonal lines between panels */}
|
||||
<svg className="absolute inset-0 h-full w-full z-10 pointer-events-none" preserveAspectRatio="none" viewBox="0 0 100 100">
|
||||
<line x1="38" y1="0" x2="33" y2="100" stroke="rgba(201,169,110,0.25)" strokeWidth="0.15" />
|
||||
<line x1="69" y1="0" x2="64" y2="100" stroke="rgba(201,169,110,0.25)" strokeWidth="0.15" />
|
||||
</svg>
|
||||
</div>
|
||||
<source src={centerVideo} type="video/mp4" />
|
||||
</video>
|
||||
<div className="absolute inset-0 bg-black/50" />
|
||||
</div>
|
||||
|
||||
{/* Desktop: diagonal split with all videos */}
|
||||
<div className="absolute inset-0 hidden md:block">
|
||||
{videos.slice(0, 3).map((src, i) => {
|
||||
const positions = [
|
||||
{ left: "0%", width: "38%" },
|
||||
{ left: "31%", width: "38%" },
|
||||
{ left: "62%", width: "38%" },
|
||||
];
|
||||
const clips = [
|
||||
"polygon(0 0, 100% 0, 86% 100%, 0 100%)",
|
||||
"polygon(14% 0, 100% 0, 86% 100%, 0 100%)",
|
||||
"polygon(14% 0, 100% 0, 100% 100%, 0 100%)",
|
||||
];
|
||||
return (
|
||||
<div
|
||||
key={src}
|
||||
className="absolute top-0 bottom-0 overflow-hidden"
|
||||
style={{
|
||||
left: positions[i].left,
|
||||
width: positions[i].width,
|
||||
clipPath: clips[i],
|
||||
}}
|
||||
>
|
||||
<video
|
||||
autoPlay muted loop playsInline preload="auto"
|
||||
onCanPlayThrough={handleVideoReady}
|
||||
className="absolute inset-0 h-full w-full object-cover object-center"
|
||||
>
|
||||
<source src={src} type="video/mp4" />
|
||||
</video>
|
||||
<div className="absolute inset-0 bg-black/50" />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* Gold diagonal lines between panels */}
|
||||
<svg className="absolute inset-0 h-full w-full z-10 pointer-events-none" preserveAspectRatio="none" viewBox="0 0 100 100">
|
||||
<line x1="38" y1="0" x2="33" y2="100" stroke="rgba(201,169,110,0.25)" strokeWidth="0.15" />
|
||||
<line x1="69" y1="0" x2="64" y2="100" stroke="rgba(201,169,110,0.25)" strokeWidth="0.15" />
|
||||
</svg>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Loading overlay — covers videos but not content */}
|
||||
<div
|
||||
ref={overlayRef}
|
||||
className="absolute inset-0 z-[5] bg-[#050505] pointer-events-none transition-opacity duration-1000"
|
||||
/>
|
||||
|
||||
{/* Floating hearts */}
|
||||
<FloatingHearts />
|
||||
|
||||
Reference in New Issue
Block a user