updated slider
@@ -18,6 +18,7 @@
|
||||
"react-router-dom": "^6.23.1",
|
||||
"react-router-hash-link": "^2.4.3",
|
||||
"react-swipeable": "^7.0.1",
|
||||
"react-usestateref": "^1.0.9",
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"zustand": "^4.5.4"
|
||||
},
|
||||
|
||||
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 551 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 867 KiB After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
@@ -94,7 +94,7 @@ function Figure({
|
||||
backgroundSize: 'auto,100% 100%',
|
||||
transition: { duration: 0.075 },
|
||||
}}
|
||||
className="flex px-6 2xl:max-w-[22vw] w-full rounded-2xl pt-6 bg-no-repeat bg-[position:bottom_right_24px,bottom_right] h-[262px] relative before:bg-[#14161F] before:w-full after:h-full after:-mt-6 after:-ml-6 after:absolute after:-z-[9999] after:rounded-2xl"
|
||||
className="flex px-6 2xl:max-w-[22vw] w-full rounded-2xl pt-6 bg-no-repeat bg-[position:bottom_right_24px,bottom_right] h-[262px] relative"
|
||||
>
|
||||
<div className="flex flex-col justify-between py-6 max-sm:max-w-[50vw]">
|
||||
<h3 className="lg:font-medium l-text 2xl:max-w-[70%]">{title}</h3>
|
||||
|
||||
@@ -35,7 +35,7 @@ export function Products() {
|
||||
<MiniTitle className="lg:hidden" text="Продукты" />
|
||||
<div
|
||||
className={
|
||||
'relative flex gax-y-4 bg-[#14161F] before:bg-[#3D425C4D] before:absolute before:w-full before:h-full before:-m-1 [&::-webkit-scrollbar]:hidden rounded-xl p-1 mb-2 w-fit max-w-full overflow-auto sm:max-lg:mt-[13px] mt-6' +
|
||||
'flex gax-y-4 bg-[#3D425C4D] [&::-webkit-scrollbar]:hidden rounded-xl p-1 mb-2 w-fit max-w-full overflow-auto sm:max-lg:mt-[13px] mt-6' +
|
||||
(curTab !== 2
|
||||
? ' max-[912px]:[-webkit-mask-image:_linear-gradient(to_left,rgba(32,35,50,0)_0%,rgba(32,35,50,1)_20%)]'
|
||||
: '')
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useInView } from 'framer-motion';
|
||||
|
||||
export function TrainingsTab() {
|
||||
return (
|
||||
<div className="bg-[#14161F] before:bg-[#3D425C4D] before:absolute before:w-full before:h-full before:rounded-xl lg:before:-m-10 rounded-xl lg:aspect-[1520/546] sm:aspect-[720/460] lg:h-[max(534px,34vw)] md:h-[min(460px,60vw)] sm:h-[max(460px,60vw)] w-full lg:bg-[url('src/assets/products/trainings/trainings.png')] 2xl:bg-[length:70%] bg-right-bottom lg:bg-[length:55%] lg:p-10 sm:p-7 p-5 bg-no-repeat">
|
||||
<div className="bg-[#3D425C4D] rounded-xl lg:aspect-[1520/546] sm:aspect-[720/460] lg:h-[max(534px,34vw)] md:h-[min(460px,60vw)] sm:h-[max(460px,60vw)] w-full lg:bg-[url('src/assets/products/trainings/trainings.png')] 2xl:bg-[length:70%] bg-right-bottom lg:bg-[length:55%] lg:p-10 sm:p-7 p-5 bg-no-repeat">
|
||||
<div className="lg:max-w-[max(455px,28vw)]">
|
||||
<div className="sm:max-lg:border-b border-[#3D425C] sm:max-lg:bg-[url('src/assets/products/trainings/trainings.png')] bg-no-repeat md:bg-contain bg-right-bottom sm:bg-[length:40%]">
|
||||
<div className="sm:max-lg:max-w-[51vw] max-sm:border-b border-[#3D425C]">
|
||||
|
||||
@@ -5,6 +5,21 @@ 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 (
|
||||
@@ -20,19 +35,49 @@ export function Projects() {
|
||||
<Slider
|
||||
projects={[
|
||||
{
|
||||
src: 'src/assets/projects/tank.jpg',
|
||||
tags: ['Симулятор', 'VR-приложение'],
|
||||
title: 'Ремонт и обслуживание двигателей спецтехники',
|
||||
src: 'src/assets/projects/loader.png',
|
||||
tags: ['Симулятор'],
|
||||
title: 'Симулятор погрузчика',
|
||||
media: Media.img,
|
||||
},
|
||||
{
|
||||
src: 'src/assets/projects/helicopter.jpg',
|
||||
tags: ['Симулятор'],
|
||||
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,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
@@ -40,22 +85,62 @@ export function Projects() {
|
||||
);
|
||||
}
|
||||
|
||||
function Project({
|
||||
src,
|
||||
title,
|
||||
tags,
|
||||
}: {
|
||||
src: string;
|
||||
title: string;
|
||||
tags: string[];
|
||||
}) {
|
||||
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)] pointer-events-none flex flex-col">
|
||||
<div
|
||||
className="bg-cover bg-center bg-no-repeat min-h-[340px] flex-1 rounded-2xl"
|
||||
style={{ backgroundImage: `url(${src})` }}
|
||||
/>
|
||||
<div className="p-5 flex flex-col justify-between gap-3 h-fit">
|
||||
<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 => (
|
||||
@@ -72,11 +157,7 @@ function Project({
|
||||
);
|
||||
}
|
||||
|
||||
function Slider({
|
||||
projects,
|
||||
}: {
|
||||
projects: { src: string; title: string; tags: string[] }[];
|
||||
}) {
|
||||
function Slider({ projects }: { projects: IProject<Media>[] }) {
|
||||
const width = useWindowWidth();
|
||||
const baseOffset =
|
||||
width >= 1024 ? width * 0.39 : width >= 640 ? width * 0.67 : width - 20;
|
||||
@@ -90,10 +171,10 @@ function Slider({
|
||||
switch (action) {
|
||||
case 'prev':
|
||||
setSliderOffset(prev => prev - baseOffset);
|
||||
return [state[state.length - 2], ...state.slice(0, -1)];
|
||||
return [state[state.length - 5], ...state.slice(0, -1)];
|
||||
case 'next':
|
||||
setSliderOffset(prev => prev + baseOffset);
|
||||
return [...state.slice(1), state[state.length - 3]];
|
||||
return [...state.slice(1), state[4]];
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@@ -174,7 +255,7 @@ function Slider({
|
||||
<div className="h-1 bg-[#3D425C] w-full">
|
||||
<div
|
||||
className="bg-[#ffffff] h-1 duration-500"
|
||||
style={{ width: `${((slide + 1) / 3) * 100}%` }}
|
||||
style={{ width: `${((slide + 1) / projects.length) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export function PauseIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path d="M10 3H7L5 5V21H8L10 19V3Z" fill="white" />
|
||||
<path d="M19 3H16L14 5V21H17L19 19V3Z" fill="white" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1887,6 +1887,11 @@ react-swipeable@^7.0.1:
|
||||
resolved "https://registry.yarnpkg.com/react-swipeable/-/react-swipeable-7.0.1.tgz#cd299f5986c5e4a7ee979839658c228f660e1e0c"
|
||||
integrity sha512-RKB17JdQzvECfnVj9yDZsiYn3vH0eyva/ZbrCZXZR0qp66PBRhtg4F9yJcJTWYT5Adadi+x4NoG53BxKHwIYLQ==
|
||||
|
||||
react-usestateref@^1.0.9:
|
||||
version "1.0.9"
|
||||
resolved "https://registry.yarnpkg.com/react-usestateref/-/react-usestateref-1.0.9.tgz#d40bc54db116e786b6b2bb1cd20fe06e7f8187f3"
|
||||
integrity sha512-t8KLsI7oje0HzfzGhxFXzuwbf1z9vhBM1ptHLUIHhYqZDKFuI5tzdhEVxSNzUkYxwF8XdpOErzHlKxvP7sTERw==
|
||||
|
||||
react@^18.3.1:
|
||||
version "18.3.1"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
|
||||
|
||||