videos upd

This commit is contained in:
2026-03-18 18:09:35 +05:00
parent 035dd44441
commit c3522c9199
10 changed files with 109 additions and 55 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 427 KiB

After

Width:  |  Height:  |  Size: 427 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -12,7 +12,7 @@ export default function MainLayout({ children }: PropsWithChildren) {
return (
<div className="flex flex-col">
<Header />
<main className="flex-1 md:max-lg:pt-[120px] pt-[100px] md:max-lg:px-4 lg:px-[1.389vw] px-[10px] overflow-clip relative overflow-x-hidden">
<main className="flex-1 md:max-lg:pt-[120px] pt-[100px] md:max-lg:px-4 lg:px-[1.389vw] px-[10px] overflow-clip relative">
{children}
<Feedback />
</main>
+5 -7
View File
@@ -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 (
<HydrationBoundary state={dehydrate(queryClient)}>
{/* <Motivation />
<Motivation />
<Integrations />
<Presentation />
<Statistics />
<NewMap />
<Projects /> */}
<Projects />
{/* <Map /> */}
{/* <Streaming /> */}
{/* <Calculator /> */}
{/* <NewStreaming />
<Reviews /> */}
<NewStreaming />
<Reviews />
{/* <Awards /> */}
{/* <Clients /> */}
<WebMain />
<Clients />
</HydrationBoundary>
);
}
@@ -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<HTMLVideoElement>;
export default function WebHero() {
const videoRef = useRef<HTMLVideoElement>(null);
const [currentVideo, setCurrentVideo] = useState<number>(0);
const [activeVideoElement, setActiveVideoElement] = useState<0 | 1>(0);
const [playbackProgress, setPlaybackProgress] = useState<number>(0);
const [selectedVideo, setSelectedVideo] = useState<number | null>(null);
const [currentVideo, setCurrentVideo] = useState<number>(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 (
<div>
<h1
@@ -59,25 +89,22 @@ export default function WebHero() {
</h1>
<div className="max-lg:flex-col flex relative justify-center">
<div className="relative">
<AnimatePresence mode="wait">
<motion.video
key={currentVideo}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
ref={videoRef}
onEnded={onVideoEnd}
{/* Два видео для избежания мерцания при смене src */}
<WebHeroVideo
refCallback={(el) => (videoElements.current[0] = el)}
active={activeVideoElement === 0}
onEnded={handleVideoEnd}
onTimeUpdate={handleTimeUpdate}
onLoadStart={() => 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]"
/>
</AnimatePresence>
<WebHeroVideo
refCallback={(el) => (videoElements.current[1] = el)}
active={activeVideoElement === 1}
onEnded={handleVideoEnd}
onTimeUpdate={handleTimeUpdate}
onLoadStart={() => setPlaybackProgress(0)}
/>
<img
draggable={false}
className="w-[63.75vw] aspect-[918/553] z-10 mt-[3.125vw] relative max-lg:w-full max-lg:mt-0"
@@ -91,16 +118,16 @@ export default function WebHero() {
max-md:grid max-md:grid-cols-3 max-md:grid-rows-[30vw_30vw] max-md:gap-[2.222vw] max-md:mt-[4.444vw] z-10"
>
<WebHeroControlBtn
title={"Search"}
icon={<SearchIcon />}
title={"Search on the Master Plan"}
icon={<GenPlanSearchIcon />}
className="top-[14.722vw] left-0"
active={currentVideo === 0}
callback={() => onControlButtonClick(0)}
progress={currentVideo === 0 ? playbackProgress : 0}
/>
<WebHeroControlBtn
title={"Search on the Master Plan"}
icon={<GenPlanSearchIcon />}
title={"Search"}
icon={<SearchIcon />}
className="top-[20.486vw] left-[8.194vw]"
active={currentVideo === 1}
callback={() => onControlButtonClick(1)}
@@ -145,3 +172,32 @@ export default function WebHero() {
</div>
);
}
interface WebHeroVideoProps
extends React.VideoHTMLAttributes<HTMLVideoElement> {
refCallback: (el: HTMLVideoElement) => void;
active: boolean;
}
function WebHeroVideo({ refCallback, active, ...props }: WebHeroVideoProps) {
return (
<video
ref={(el) => {
if (el) refCallback(el);
}}
onEnded={props.onEnded}
onTimeUpdate={props.onTimeUpdate}
onLoadStart={props.onLoadStart}
playsInline
muted
autoPlay
style={{
opacity: active ? 1 : 0,
pointerEvents: active ? "auto" : "none",
}}
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] transition-opacity duration-300"
/>
);
}