173 lines
5.8 KiB
TypeScript
173 lines
5.8 KiB
TypeScript
import { useEffect, useMemo, useReducer, useState } from 'react';
|
||
import { MiniTitle } from '../../ui/MiniTitle';
|
||
import { useWindowWidth } from '../../hooks/useWindowWidth';
|
||
import { Title } from '../../ui/Title';
|
||
|
||
export function Projects() {
|
||
return (
|
||
<div className="desktop:py-[70px] desktop:px-10 mobile:py-14 tablet:px-6 mobile:px-4 overflow-hidden select-none">
|
||
<Title className="desktop:mb-14 mobile:mb-6">
|
||
<span
|
||
className="bg-text-gradient bg-gradient-to-r from-[#798FFF] to-[#D375FF]"
|
||
style={{
|
||
backgroundClip: 'text',
|
||
WebkitBackgroundClip: 'text',
|
||
WebkitTextFillColor: 'transparent',
|
||
}}
|
||
>
|
||
Большой опыт в работе
|
||
</span>{' '}
|
||
с промышленными предприятиями и учебными заведениями
|
||
</Title>
|
||
<MiniTitle text="реализованные проекты" />
|
||
<Slider
|
||
projects={[
|
||
{
|
||
src: 'src/assets/tank.png',
|
||
tags: ['Симулятор', 'VR-приложение'],
|
||
title: 'Ремонт и обслуживание двигателей спецтехники',
|
||
},
|
||
{
|
||
src: 'src/assets/helicopter.jpg',
|
||
tags: ['Симулятор'],
|
||
title: 'Сборка-разборка вертолётного двигателя',
|
||
},
|
||
{
|
||
src: 'src/assets/train.png',
|
||
tags: ['Симулятор'],
|
||
title: 'Симулятор машиниста',
|
||
},
|
||
]}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function Project({
|
||
src,
|
||
title,
|
||
tags,
|
||
}: {
|
||
src: string;
|
||
title: string;
|
||
tags: string[];
|
||
}) {
|
||
return (
|
||
<div className="bg-[#3D425C] bg-opacity-50 rounded-2xl box-border desktop:min-w-[624px] tablet:min-w-[520px] mobile:min-w-[328px] duration-1000 select-none desktop:translate-x-[264px]">
|
||
<div
|
||
className="bg-cover bg-center bg-no-repeat h-[340px] rounded-2xl"
|
||
style={{ backgroundImage: `url(${src})` }}
|
||
/>
|
||
<div className="p-5">
|
||
<h1 className="text-[#ffffff] font-medium tablet-figma:text-[clamp(16px,16px+(100vw-768px)/832*2,18px)] tablet-figma:leading-[19.2px,19.2px+(100vw-768px)/832*2.4,21.6px] mobile:max-tablet-figma:text-[clamp(14px,14px+(100vw-360px)/408*2,16px)] mobile:max-tablet-figma:leading-[clamp(16.8px,16.8px+(100vw-360px)/408*2,19.2px)]">
|
||
{title}
|
||
</h1>
|
||
<div className="flex gap-2 mt-4">
|
||
{tags.map(tag => (
|
||
<p
|
||
key={tag}
|
||
className="text-[#ffffff] opacity-80 font-medium rounded-3xl py-3 px-4 border border-[#798FFF] tablet-figma:text-[clamp(12px,12px+(100vw-768px)/832*2,14px)] tablet-figma:leading-[16.8px,16.8px+(100vw-768px)/832*2.8,19.6px] mobile:max-tablet-figma:text-xs mobile:max-tablet-figma:leading-[16.8px]"
|
||
>
|
||
{tag}
|
||
</p>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function Slider({
|
||
projects,
|
||
}: {
|
||
projects: { src: string; title: string; tags: string[] }[];
|
||
}) {
|
||
const width = useWindowWidth();
|
||
const baseOffset = useMemo(
|
||
() => (width >= 1024 ? 640 : width >= 640 ? 536 : 336),
|
||
[width],
|
||
);
|
||
const [sliderOffset, setSliderOffset] = useState(-baseOffset);
|
||
const [slide, setSlide] = useState(0);
|
||
|
||
const [order, dispatch] = useReducer(
|
||
(state: typeof projects, action: string) => {
|
||
if (action === 'next') {
|
||
setSliderOffset(prev => prev + baseOffset);
|
||
return [...state.slice(1), state[2]];
|
||
}
|
||
if (action === 'prev') {
|
||
setSliderOffset(-baseOffset * 2);
|
||
return [state[state.length - 3], ...state.slice(0, -1)];
|
||
}
|
||
return state;
|
||
},
|
||
[projects[projects.length - 1], ...projects, projects[0]],
|
||
);
|
||
|
||
const [touchStart, setTouchStart] = useState(0);
|
||
|
||
useEffect(() => {
|
||
setSliderOffset(-baseOffset);
|
||
}, [order, baseOffset]);
|
||
|
||
return (
|
||
<div className="flex flex-col desktop:mt-4 tablet:mt-3 mobile:mt-2">
|
||
<div
|
||
className="flex gap-2 overflow-visible relative mb-[18px] -mr-10 duration-1000"
|
||
style={{
|
||
transition: `${+!(sliderOffset === 0 || sliderOffset === -baseOffset * 2)}s`,
|
||
transform: `translateX(${sliderOffset}px)`,
|
||
}}
|
||
onTouchStart={e => {
|
||
setTouchStart(e.targetTouches[0].clientX);
|
||
}}
|
||
onTouchEnd={e => {
|
||
if (e.nativeEvent.changedTouches[0].clientX - touchStart > 100) {
|
||
dispatch('prev');
|
||
setSlide(prev => (prev === 0 ? order.length - 3 : prev - 1));
|
||
return;
|
||
} else if (
|
||
e.nativeEvent.changedTouches[0].clientX - touchStart <
|
||
-100
|
||
) {
|
||
dispatch('next');
|
||
setSlide(prev => (prev === order.length - 3 ? 0 : prev + 1));
|
||
return;
|
||
}
|
||
}}
|
||
>
|
||
{order.map((project, index) => (
|
||
<Project key={index} {...project} />
|
||
))}
|
||
</div>
|
||
<div className="flex items-center gap-4 w-[1264px] self-start ml-64">
|
||
<button
|
||
onClick={() => {
|
||
dispatch('prev');
|
||
setSlide(prev => (prev === 0 ? order.length - 3 : prev - 1));
|
||
}}
|
||
className="mobile:max-tablet:hidden"
|
||
>
|
||
<img src="src/assets/left_slide.svg" alt="" />
|
||
</button>
|
||
<div className="h-1 bg-[#3D425C] w-full">
|
||
<div
|
||
className="bg-[#ffffff] h-1 duration-1000"
|
||
style={{ width: `${((slide + 1) / 3) * 100}%` }}
|
||
/>
|
||
</div>
|
||
<button
|
||
onClick={() => {
|
||
dispatch('next');
|
||
setSlide(prev => (prev === order.length - 3 ? 0 : prev + 1));
|
||
}}
|
||
className="mobile:max-tablet:hidden"
|
||
>
|
||
<img src="src/assets/right_slide.svg" alt="" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|