268 lines
8.7 KiB
TypeScript
268 lines
8.7 KiB
TypeScript
import { createRef, useEffect, useState } from "react";
|
||
import { useInView } from "react-intersection-observer";
|
||
import { useSwipeable } from "react-swipeable";
|
||
import { Transition } from "react-transition-group";
|
||
import { Video } from "../types/Video";
|
||
import ArrowLeftIcon from "./icons/ArrowLeftIcon";
|
||
import ArrowRightIcon from "./icons/ArrowRightIcon";
|
||
|
||
const VIDEOS: Video[] = [
|
||
{
|
||
id: "1",
|
||
value: "/videos/histories/1.mp4",
|
||
title: "Интерактивный комплекс GRAFF.estate для ЖК Upside Towers, Москва",
|
||
desc: "",
|
||
poster: "/images/posters/histories/1.jpg",
|
||
},
|
||
{
|
||
id: "2",
|
||
value: "/videos/histories/2.mp4",
|
||
title: "Graff.estate на выставке 100+ TechnoBuild",
|
||
desc: "",
|
||
poster: "/images/posters/histories/2.jpg",
|
||
},
|
||
{
|
||
id: "3",
|
||
value: "/videos/histories/3.mp4",
|
||
title:
|
||
"Интерактивная инсталляция graff.estate для ЖК DNS City в г. Владивосток",
|
||
desc: "",
|
||
poster: "/images/posters/histories/3.jpg",
|
||
},
|
||
];
|
||
|
||
function Histories() {
|
||
const [selectedVideoIndex, setSelectedVideoIndex] = useState(0);
|
||
const videoRefs = VIDEOS.map(() => createRef<HTMLVideoElement>());
|
||
const [videoWidth, setVideoWidth] = useState<number>(0);
|
||
const [videoHeigth, setVideoHeigth] = useState<number>(0);
|
||
const [videoProgesses, setVideoProgresses] = useState<number[]>([]);
|
||
const { ref, inView } = useInView();
|
||
const [isEntered, setIsEntered] = useState(false);
|
||
const handlers = useSwipeable({
|
||
onSwipedLeft: next,
|
||
onSwipedRight: prev,
|
||
trackMouse: true,
|
||
});
|
||
|
||
useEffect(() => {
|
||
if (!videoRefs[0].current?.src) return;
|
||
const width = videoRefs[0].current.clientWidth;
|
||
const height = videoRefs[0].current.clientHeight;
|
||
setVideoWidth(width);
|
||
setVideoHeigth(height);
|
||
}, [videoRefs[0]]);
|
||
|
||
function handleEnded() {
|
||
if (selectedVideoIndex === VIDEOS.length - 1) {
|
||
setSelectedVideoIndex(0);
|
||
} else {
|
||
setSelectedVideoIndex((prev) => prev + 1);
|
||
}
|
||
}
|
||
|
||
function prev() {
|
||
if (selectedVideoIndex === 0) return;
|
||
setSelectedVideoIndex((prev) => prev - 1);
|
||
}
|
||
|
||
function next() {
|
||
if (selectedVideoIndex === VIDEOS.length - 1) return;
|
||
setSelectedVideoIndex((prev) => prev + 1);
|
||
}
|
||
|
||
useEffect(() => {
|
||
const progresses = VIDEOS.map(() => 0);
|
||
setVideoProgresses(progresses);
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
const interval = setInterval(() => {
|
||
const updatedProgresses = videoRefs.map((currentVideoRef) => {
|
||
if (!currentVideoRef.current) return 0;
|
||
|
||
return (
|
||
(100 / currentVideoRef.current.duration) *
|
||
currentVideoRef.current.currentTime
|
||
);
|
||
});
|
||
|
||
setVideoProgresses(updatedProgresses);
|
||
}, 1000);
|
||
|
||
return () => clearInterval(interval);
|
||
}, [selectedVideoIndex, videoRefs]);
|
||
|
||
useEffect(() => {
|
||
if (!inView || isEntered) return;
|
||
|
||
setIsEntered(true);
|
||
}, [inView]);
|
||
|
||
useEffect(() => {
|
||
for (let index = 0; index < videoRefs.length; index++) {
|
||
if (selectedVideoIndex === index) {
|
||
videoRefs[index].current?.play();
|
||
} else {
|
||
videoRefs[index].current?.pause();
|
||
}
|
||
}
|
||
}, [isEntered, selectedVideoIndex]);
|
||
|
||
return (
|
||
<div
|
||
ref={ref}
|
||
className="container mx-auto 2xl:max-w-screen-2xl mb-[120px]"
|
||
>
|
||
<div className="flex 2xl:h-[760px] xl:h-[687px] sm:h-[500px] h-[875px] sm:border-b border-b-[#3D425C] sm:flex-row flex-col">
|
||
<div className="left 2xl:min-w-[496px] xl:min-w-[395px] sm:min-w-[354px] flex flex-col sm:pb-10 pb-6">
|
||
<h2 className="font-medium 2xl:text-[64px] xl:text-5xl text-[40px] sm:mb-10 mb-6 font-gilroy leading-10">
|
||
<p className="from-[#798FFF] to-[#D375FF] bg-gradient-to-r bg-clip-text text-transparent">
|
||
Истории
|
||
</p>
|
||
<p>graff.estate</p>
|
||
</h2>
|
||
<div className="relative bg-white sm:block hidden">
|
||
{VIDEOS.map((item, index) => (
|
||
<Transition
|
||
key={index}
|
||
in={Boolean(index === selectedVideoIndex)}
|
||
timeout={300}
|
||
>
|
||
{(state) => (
|
||
<div
|
||
className={`absolute transition-opacity duration-300 pr-10 ${state}`}
|
||
>
|
||
{item.title ? (
|
||
<p className="font-semibold mb-4 2xl:text-xl xl:text-[16px]">
|
||
{item.title}
|
||
</p>
|
||
) : (
|
||
<></>
|
||
)}
|
||
<p className="font-normal 2xl:text-[16px] xl:text-[14px]">
|
||
{item.desc}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</Transition>
|
||
))}
|
||
</div>
|
||
<div className="gap-2 hidden sm:flex mt-auto">
|
||
<button
|
||
className="p-4 border border-[#3D425C] rounded-full"
|
||
onClick={prev}
|
||
>
|
||
<ArrowLeftIcon />
|
||
</button>
|
||
<button
|
||
className="p-4 border border-[#3D425C] rounded-full"
|
||
onClick={next}
|
||
>
|
||
<ArrowRightIcon />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div
|
||
{...handlers}
|
||
className="right relative sm:border-l border-l-[#3D425C] flex gap-4 sm:pb-10 sm:pl-10 select-none"
|
||
style={{
|
||
clipPath: `rect(auto auto auto 0)`,
|
||
}}
|
||
>
|
||
{VIDEOS.map((video, index) => (
|
||
<div
|
||
key={video.id}
|
||
className="relative aspect-[9/16] bg-black transition-transform duration-300 sm:h-auto sm:min-w-0 h-[546px] min-w-[100vw]"
|
||
style={{
|
||
transform: `translateX(-${
|
||
videoWidth * selectedVideoIndex + 16 * selectedVideoIndex
|
||
}px)`,
|
||
}}
|
||
>
|
||
<video
|
||
ref={videoRefs[index]}
|
||
src={isEntered ? video.value : undefined}
|
||
poster={video.poster}
|
||
muted
|
||
playsInline
|
||
className="aspect-[9/16] object-cover w-full h-full min-w-screen"
|
||
onEnded={handleEnded}
|
||
// onLoadedData={() =>
|
||
// setTimeout(() => {
|
||
// setVideoWidth(videoRefs[0].current!.clientWidth);
|
||
// setVideoHeigth(videoRefs[0].current!.clientHeight);
|
||
// }, 200)
|
||
// }
|
||
/>
|
||
<div
|
||
className={`absolute progress-bar h-[3px] bg-[#52587A] transition-opacity duration-500 sm:block hidden ${
|
||
selectedVideoIndex === index ? "opacity-100" : "opacity-0"
|
||
}`}
|
||
style={{
|
||
width: `${videoWidth}px`,
|
||
top: `${videoHeigth - 3}px`,
|
||
}}
|
||
>
|
||
<div
|
||
className="bg-white h-full transition-[width] duration-300"
|
||
style={{
|
||
width: `${videoProgesses[index]}%`,
|
||
}}
|
||
></div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div className="relative block sm:hidden h-[100%] bg-[#212431] p-6">
|
||
{VIDEOS.map((item, index) => (
|
||
<Transition
|
||
key={index}
|
||
in={Boolean(index === selectedVideoIndex)}
|
||
timeout={300}
|
||
>
|
||
{(state) => (
|
||
<div
|
||
className={`absolute transition-opacity duration-300 pr-10 ${state}`}
|
||
>
|
||
{item.title ? (
|
||
<p className="font-semibold mb-4 2xl:text-xl xl:text-[16px]">
|
||
{item.title}
|
||
</p>
|
||
) : (
|
||
<></>
|
||
)}
|
||
<p className="font-normal 2xl:text-[16px] xl:text-[14px]">
|
||
{item.desc}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</Transition>
|
||
))}
|
||
</div>
|
||
<div className="mx-auto flex gap-2 min-h-2 sm:hidden relative mt-6">
|
||
<div className="bg-[#a1a2a6] w-10 h-full rounded-full overflow-hidden">
|
||
<div
|
||
className="bg-white w-0 h-full rounded-full transition-all duration-500"
|
||
style={{ width: `${videoProgesses[selectedVideoIndex]}%` }}
|
||
></div>
|
||
</div>
|
||
{VIDEOS.map((video, index) => (
|
||
<div
|
||
key={video.id}
|
||
className="h-full w-2 rounded-full transition-all duration-300"
|
||
style={{
|
||
background: `${
|
||
selectedVideoIndex === index ? "white" : "#a1a2a6"
|
||
}`,
|
||
}}
|
||
/>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default Histories;
|