Files
graff.training/src/components/Main/Projects.tsx
T
2024-08-20 19:01:03 +05:00

277 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useReducer, useRef, useState } from 'react';
import { MiniTitle } from '../../ui/MiniTitle';
import { useWindowWidth } from '../../hooks/useWindowWidth';
import { Title } from '../../ui/Title';
import { useSwipeable } from 'react-swipeable';
import { ArrowLeftIcon } from '../icons/ArrowLeftIcon';
import { ArrowRightIcon } from '../icons/ArrowRightIcon';
import { PauseIcon } from '../icons/PauseIcon';
import { PlayIcon } from '../icons/PlayIcon';
import useStateRef from 'react-usestateref';
enum Media {
video,
img,
}
interface IProject<TMedia extends Media> {
src: TMedia extends Media.img ? string : string[];
title: string;
tags: string[];
media?: TMedia;
}
export function Projects() {
return (
<div
id="projects"
className="lg:py-[70px] lg:px-10 py-14 sm:px-6 px-4 overflow-hidden"
>
<Title className="2xl:mb-[77px] lg:mb-14 mb-6">
<span className="text-gradient">Большой опыт в работе</span> с
промышленными предприятиями и учебными заведениями
</Title>
<MiniTitle text="реализованные проекты" />
<Slider
projects={[
{
src: 'src/assets/projects/loader.png',
tags: ['Симулятор'],
title: 'Симулятор погрузчика',
media: Media.img,
},
{
src: [
'src/assets/projects/operator.mp4',
'src/assets/projects/operator.png',
],
tags: ['Симулятор', 'VR-приложение'],
title: 'Обучение работе с системой водоочистки',
media: Media.video,
} as IProject<Media.video>,
{
src: 'src/assets/projects/plane.png',
tags: ['Симулятор', 'VR-приложение'],
title: 'L 410 NG Aircraft',
media: Media.img,
},
{
src: 'src/assets/projects/hangar.png',
tags: ['Симулятор', 'VR-приложение'],
title: 'Сборка-разборка вертолётного двигателя',
media: Media.img,
},
{
src: 'src/assets/projects/trains.png',
tags: ['Симулятор', 'VR-приложение'],
title: 'Тренажер РЖД: ЭП2Д, Иволга, ЭП20, ТЭ33А, ТЭМ2',
media: Media.img,
},
{
src: 'src/assets/projects/laboratory.png',
tags: ['Симулятор', 'VR-приложение'],
title: 'Учебная лаборатория определения жирности молока',
media: Media.img,
},
{
src: 'src/assets/projects/train_big.jpg',
tags: ['Симулятор'],
title: 'Симулятор машиниста',
media: Media.img,
},
]}
/>
</div>
);
}
function Project<T extends Media>({ src, title, tags, media }: IProject<T>) {
const [buffering, setBuffering] = useState(true);
const [playing, setPlaying, playingRef] = useStateRef(false);
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => console.log(buffering), [buffering]);
useEffect(() => console.log(playingRef), [playingRef]);
useEffect(() => console.log(playing), [playing]);
return (
<div className="bg-[#3D425C] bg-opacity-50 rounded-2xl aspect-[624/462] lg:min-w-[39vw] sm:min-w-[67vw] min-w-[calc(100vw-32px)] lg:translate-x-[calc(15.5vw-32px)] flex flex-col relative">
{media === Media.img ? (
<div
className="bg-cover bg-center bg-no-repeat min-h-[340px] flex-1 rounded-t-2xl"
style={{ backgroundImage: `url(${src})` }}
/>
) : (
<div
className="flex-1 rounded-t-2xl overflow-hidden relative flex justify-center items-center group bg-cover bg-center bg-no-repeat"
style={{ backgroundImage: `url(${src[1]})` }}
>
<video
src={src[0]}
ref={videoRef}
muted
loop
className="rounded-t-2xl object-cover aspect-[495/340]"
onPlaying={() => setBuffering(false)}
onWaiting={() => setBuffering(true)}
/>
<button
onClick={() => {
if (playingRef.current) {
videoRef.current?.pause();
setPlaying(false);
} else {
videoRef.current?.play();
setPlaying(true);
}
}}
className={
'absolute p-4 rounded-full border border-white group-hover:block ' +
(!buffering && playing ? 'hidden' : 'block')
}
>
{!buffering && playing ? (
<PauseIcon className="w-4 h-4" />
) : (
<PlayIcon className="w-4 h-4" />
)}
</button>
{buffering && <div className="rounded-t-2xl absolute" />}
</div>
)}
<div className="p-5 flex flex-col justify-between gap-3">
<h4 className="font-medium h4">{title}</h4>
<div className="flex gap-2">
{tags.map(tag => (
<p
key={tag}
className="opacity-80 font-medium rounded-3xl sm:py-3 py-2 px-4 border border-[#798FFF] m-text"
>
{tag}
</p>
))}
</div>
</div>
</div>
);
}
function Slider({ projects }: { projects: IProject<Media>[] }) {
const width = useWindowWidth();
const baseOffset =
width >= 1024 ? width * 0.39 : width >= 640 ? width * 0.67 : width - 20;
const [sliderOffset, setSliderOffset] = useState(-baseOffset);
const [slide, setSlide] = useState(0);
const [isAnimating, setIsAnimating] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const [order, dispatch] = useReducer(
(state: typeof projects, action: string) => {
switch (action) {
case 'prev':
setSliderOffset(prev => prev - baseOffset);
return [state[state.length - 5], ...state.slice(0, -1)];
case 'next':
setSliderOffset(prev => prev + baseOffset);
return [...state.slice(1), state[4]];
default:
return state;
}
},
[
projects[projects.length - 2],
projects[projects.length - 1],
...projects,
projects[0],
projects[1],
],
);
const handlers = useSwipeable({
onSwipedLeft: () => {
if (!isAnimating) {
setIsAnimating(true);
setSlide(prev => (prev === order.length - 5 ? 0 : prev + 1));
dispatch('next');
}
},
onSwipedRight: () => {
if (!isAnimating) {
setIsAnimating(true);
setSlide(prev => (prev === 0 ? order.length - 5 : prev - 1));
dispatch('prev');
}
},
trackMouse: true,
preventScrollOnSwipe: true,
touchEventOptions: { passive: false },
});
useEffect(() => {
setSliderOffset(-baseOffset * 2);
}, [order, baseOffset, slide]);
useEffect(() => {
const refValue = ref.current;
refValue?.addEventListener('transitionend', () => {
setIsAnimating(false);
});
return () =>
refValue?.removeEventListener('transitionend', () =>
setIsAnimating(false),
);
}, [sliderOffset, order, slide]);
return (
<div className="flex flex-col lg:mt-4 sm:mt-3 mt-2">
<div {...handlers}>
<div
ref={ref}
className="flex sm:gap-4 gap-2 overflow-visible relative mb-[18px] -mr-10 select-none"
style={{
transition: `${sliderOffset === -baseOffset || sliderOffset === -baseOffset * 3 ? 0 : 0.5}s`,
transform: `translateX(${sliderOffset}px)`,
}}
>
{order.map((project, index) => (
<Project key={index} {...project} />
))}
</div>
</div>
<div className="flex items-center gap-4 lg:w-[clamp(720px,100vw-465px,1135px)] 2xl:w-[70.9vw] w-full self-start lg:ml-[15.5vw]">
<button
onClick={() => {
if (!isAnimating) {
setIsAnimating(true);
setSlide(prev => (prev === 0 ? order.length - 5 : prev - 1));
dispatch('prev');
}
}}
className="max-sm:hidden outline-none"
>
<ArrowLeftIcon />
</button>
<div className="h-1 bg-[#3D425C] w-full">
<div
className="bg-[#ffffff] h-1 duration-500"
style={{ width: `${((slide + 1) / projects.length) * 100}%` }}
/>
</div>
<button
onClick={() => {
if (!isAnimating) {
setIsAnimating(true);
setSlide(prev => (prev === order.length - 5 ? 0 : prev + 1));
dispatch('next');
}
}}
className="max-sm:hidden outline-none"
>
<ArrowRightIcon />
</button>
</div>
</div>
);
}