diff --git a/public/img/components/products/streaming-notebook.png b/public/img/components/products/streaming-notebook.png index c229968..0a999ba 100644 Binary files a/public/img/components/products/streaming-notebook.png and b/public/img/components/products/streaming-notebook.png differ diff --git a/public/videos/pages/web/hero_0.mp4 b/public/videos/pages/web/hero_0.mp4 index a1e844a..778dd1b 100644 Binary files a/public/videos/pages/web/hero_0.mp4 and b/public/videos/pages/web/hero_0.mp4 differ diff --git a/public/videos/pages/web/hero_1.mp4 b/public/videos/pages/web/hero_1.mp4 index a170ec9..9579a85 100644 Binary files a/public/videos/pages/web/hero_1.mp4 and b/public/videos/pages/web/hero_1.mp4 differ diff --git a/public/videos/pages/web/hero_2.mp4 b/public/videos/pages/web/hero_2.mp4 index d651a20..8e19fab 100644 Binary files a/public/videos/pages/web/hero_2.mp4 and b/public/videos/pages/web/hero_2.mp4 differ diff --git a/public/videos/pages/web/hero_3.mp4 b/public/videos/pages/web/hero_3.mp4 index 5b5c43f..68bbcb1 100644 Binary files a/public/videos/pages/web/hero_3.mp4 and b/public/videos/pages/web/hero_3.mp4 differ diff --git a/public/videos/pages/web/hero_4.mp4 b/public/videos/pages/web/hero_4.mp4 index cd35dd1..a387e62 100644 Binary files a/public/videos/pages/web/hero_4.mp4 and b/public/videos/pages/web/hero_4.mp4 differ diff --git a/public/videos/pages/web/hero_5.mp4 b/public/videos/pages/web/hero_5.mp4 index 1ef2dc6..6cebbff 100644 Binary files a/public/videos/pages/web/hero_5.mp4 and b/public/videos/pages/web/hero_5.mp4 differ diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index cce44a6..5629c9a 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -12,7 +12,7 @@ export default function MainLayout({ children }: PropsWithChildren) { return (
-
+
{children}
diff --git a/src/app/(main)/page.tsx b/src/app/(main)/page.tsx index df1cc6b..44a9103 100644 --- a/src/app/(main)/page.tsx +++ b/src/app/(main)/page.tsx @@ -15,7 +15,6 @@ import { queryCompaniesOptions } from "@/queries/getCompanies"; import { queryCompaniesCountOptions } from "@/queries/getCompaniesCount"; import NewStreaming from "@/components/pages/MainPage/NewStreaming"; import NewMap from "@/components/pages/MainPage/Map/NewMap"; -import WebMain from "@/components/pages/WebPage/WebMain"; export default async function HomePage() { const queryClient = new QueryClient(); @@ -55,20 +54,19 @@ export default async function HomePage() { return ( - {/* + - */} + {/* */} {/* */} {/* */} - {/* - */} + + {/* */} - {/* */} - + ); } diff --git a/src/components/pages/WebPage/PageBlocks/WebHero.tsx b/src/components/pages/WebPage/PageBlocks/WebHero.tsx index 1d28298..cfb9ed1 100644 --- a/src/components/pages/WebPage/PageBlocks/WebHero.tsx +++ b/src/components/pages/WebPage/PageBlocks/WebHero.tsx @@ -1,49 +1,79 @@ /* eslint-disable @next/next/no-img-element */ "use client"; -import React, { useCallback, useEffect, useRef, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import SearchIcon from "@/icons/SearchIcon"; import GenPlanSearchIcon from "@/components/icons/GenPlanSearchIcon"; import TreeIcon from "@/components/icons/TreeIcon"; import FavoriteIcon from "@/components/icons/FavoriteIcon"; import WebHeroControlBtn from "../PageComponents/WebHeroControlBtn"; -import { AnimatePresence, motion } from "framer-motion"; import ThreeDTourIcon from "@/components/icons/3DTourIcon"; import InfoIcon from "@/components/icons/InfoIcon"; +const VIDEO_COUNT = 6; +type VIDEO_EVENT = React.SyntheticEvent; + export default function WebHero() { - const videoRef = useRef(null); - const [currentVideo, setCurrentVideo] = useState(0); + const [activeVideoElement, setActiveVideoElement] = useState<0 | 1>(0); const [playbackProgress, setPlaybackProgress] = useState(0); - const [selectedVideo, setSelectedVideo] = useState(null); + const [currentVideo, setCurrentVideo] = useState(0); // 0 | 1 | 2 | 3 | 4 | 5 + const videoElements = useRef<(HTMLVideoElement | null)[]>([null, null]); + const isInitialLoad = useRef(true); - const updateProgress = useCallback(() => { - if (!videoRef.current) return; - const { currentTime, duration } = videoRef.current; - const progressPercent = duration ? (currentTime / duration) * 100 : 0; + function handleTimeUpdate(e: VIDEO_EVENT) { + if (e.currentTarget !== videoElements.current[activeVideoElement]) return; - setPlaybackProgress(progressPercent); - }, [currentVideo]); + const { currentTime, duration } = e.currentTarget; + const progressPercentage = duration ? (currentTime / duration) * 100 : 0; + setPlaybackProgress(progressPercentage); + } - const handleTimeUpdate = useCallback(() => { - updateProgress(); - }, [updateProgress]); - - useEffect(() => { - setPlaybackProgress(0); - }, [currentVideo]); - - const onVideoEnd = () => { - setCurrentVideo((cur) => (cur + 1) % 6); - }; + function handleVideoEnd(e: VIDEO_EVENT) { + if (e.currentTarget !== videoElements.current[activeVideoElement]) return; + setCurrentVideo((cur) => (cur + 1) % VIDEO_COUNT); + } function onControlButtonClick(video: number) { - if (selectedVideo === video) setSelectedVideo(null); + if (currentVideo === video) return; else { setCurrentVideo(video); - setSelectedVideo(video); + setPlaybackProgress(0); } } + // Как только произошло переключение на новое видео (изменился currentVideo) + useEffect(() => { + // indexOfVideoToPlay = 0 || 1, используется для определения индекса видео, которое нужно загрузить. При первой загрузке index = 0 + const indexOfVideoToPlay = isInitialLoad.current + ? 0 + : ((1 - activeVideoElement) as 0 | 1); + isInitialLoad.current = false; + + // Получаем видео, которое будет проиграно + const videoToPlay = videoElements.current[indexOfVideoToPlay]; + if (!videoToPlay) return; + + const handleCanPlay = () => { + // Как только можем проиграть текущее видео, останавливаем то, которое было до него + const currentlyPlayingVideo = + videoElements.current[1 - indexOfVideoToPlay]; + currentlyPlayingVideo?.pause(); + + // Запускаем текущее видео + videoToPlay.currentTime = 0; + videoToPlay.play().catch(() => {}); + setActiveVideoElement(indexOfVideoToPlay); + }; + + videoToPlay.src = `/videos/pages/web/hero_${currentVideo}.mp4`; + videoToPlay.addEventListener("canplaythrough", handleCanPlay, { + once: true, + }); + + return () => { + videoToPlay.removeEventListener("canplaythrough", handleCanPlay); + }; + }, [currentVideo]); + return (

- - setPlaybackProgress(0)} - playsInline - muted - autoPlay - src={`/videos/pages/web/hero_${currentVideo}.mp4`} - className="absolute w-[50.694vw] aspect-[730/472] top-[4vw] left-[6.458vw] - md:max-lg:w-[87.2%] max-lg:md:aspect-[585/345] md:max-lg:top-[1.5vw] md:max-lg:left-[6.458vw] - max-md:w-[80.2%] max-md:left-[9.444vw] max-md:top-[1.444vw]" - /> - + {/* Два видео для избежания мерцания при смене src */} + (videoElements.current[0] = el)} + active={activeVideoElement === 0} + onEnded={handleVideoEnd} + onTimeUpdate={handleTimeUpdate} + onLoadStart={() => setPlaybackProgress(0)} + /> + (videoElements.current[1] = el)} + active={activeVideoElement === 1} + onEnded={handleVideoEnd} + onTimeUpdate={handleTimeUpdate} + onLoadStart={() => setPlaybackProgress(0)} + /> + } + title={"Search on the Master Plan"} + icon={} className="top-[14.722vw] left-0" active={currentVideo === 0} callback={() => onControlButtonClick(0)} progress={currentVideo === 0 ? playbackProgress : 0} /> } + title={"Search"} + icon={} className="top-[20.486vw] left-[8.194vw]" active={currentVideo === 1} callback={() => onControlButtonClick(1)} @@ -145,3 +172,32 @@ export default function WebHero() {
); } + +interface WebHeroVideoProps + extends React.VideoHTMLAttributes { + refCallback: (el: HTMLVideoElement) => void; + active: boolean; +} + +function WebHeroVideo({ refCallback, active, ...props }: WebHeroVideoProps) { + return ( +