178 lines
5.4 KiB
TypeScript
178 lines
5.4 KiB
TypeScript
import { AnimatePresence, motion } from 'framer-motion';
|
||
import { devices } from '../consts/devices';
|
||
import { IDevice } from '../types/IDevice';
|
||
import { Title } from './ui/Title';
|
||
import { useEffect, useRef, useState } from 'react';
|
||
import { useHover, useOnClickOutside } from 'usehooks-ts';
|
||
import { ChevronUpIcon } from './icons/ChevronUpIcon';
|
||
import { ChevronDownIcon } from './icons/ChevronDownIcon';
|
||
|
||
export function Devices() {
|
||
return (
|
||
<div
|
||
id="devices"
|
||
className="space-y-8 scroll-m-14 lg:scroll-m-20 sm:space-y-10 lg:space-y-20 lg:pt-[180px] sm:pt-[140px] pt-20"
|
||
>
|
||
<Title>
|
||
Работаем с
|
||
<span className="text-gradient"> любыми типами оборудования</span>
|
||
<br /> и предложим лучшее мультимедийное оснащение
|
||
</Title>
|
||
<div className="max-lg:hidden">
|
||
{devices.map((device, index) => (
|
||
<DesktopDevice key={device.title} {...device} number={index + 1} />
|
||
))}
|
||
</div>
|
||
<div className="lg:hidden">
|
||
{devices.map(device => (
|
||
<Device key={device.title} {...device} />
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function DesktopDevice({
|
||
title,
|
||
description,
|
||
img,
|
||
number,
|
||
}: IDevice & {
|
||
number: number;
|
||
}) {
|
||
const root = useRef<HTMLDivElement>(null);
|
||
const descriptionRef = useRef<HTMLParagraphElement>(null);
|
||
|
||
const [descriptionHeight, setDescriptionHeight] = useState(0);
|
||
|
||
const hovered = useHover(root);
|
||
|
||
useEffect(() => {
|
||
setDescriptionHeight(descriptionRef.current?.clientHeight ?? 0);
|
||
}, [descriptionRef, hovered]);
|
||
|
||
return (
|
||
<motion.div
|
||
ref={root}
|
||
className="py-10 border-b border-[#3D425C] flex justify-between items-start relative [clip-path:polygon(0%_-50%,100%_-50%,100%_100%,-50%_100%)]"
|
||
style={{ maxHeight: 112 + descriptionHeight + 21 }}
|
||
animate={{
|
||
height: hovered
|
||
? (root.current?.clientHeight ?? 0) + descriptionHeight + 24
|
||
: 112,
|
||
}}
|
||
transition={{ delay: 0.3 }}
|
||
>
|
||
<div className="space-y-6">
|
||
<p className="font-medium h3">{title}</p>
|
||
<AnimatePresence>
|
||
{hovered && (
|
||
<motion.div
|
||
ref={descriptionRef}
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 0.6 }}
|
||
exit={{ opacity: 0 }}
|
||
transition={{ delay: 0.3 }}
|
||
className="l-text space-y-4 max-w-[calc(600/1552*100%)] absolute"
|
||
>
|
||
{description.map(paragraph => (
|
||
<p key={paragraph}>
|
||
{paragraph}
|
||
<br />
|
||
</p>
|
||
))}
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
<AnimatePresence>
|
||
{hovered && (
|
||
<motion.img
|
||
src={img}
|
||
alt={title}
|
||
className="bottom-0 right-[calc(144/1552*100%)] w-[calc(560/1552*100%)] max-w-[560px] absolute"
|
||
initial={{ opacity: 0, y: 500 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: 100 }}
|
||
transition={{ duration: 0.5, delay: 0.3 }}
|
||
/>
|
||
)}
|
||
</AnimatePresence>
|
||
<p className="m-text text-[#52587A] font-medium">[0{number}]</p>
|
||
</motion.div>
|
||
);
|
||
}
|
||
|
||
function Device({ title, description, img }: IDevice) {
|
||
const [expanded, setExpanded] = useState(false);
|
||
const [descriptionHeight, setDescriptionHeight] = useState(0);
|
||
const [imgHeight, setImgHeight] = useState(0);
|
||
|
||
const root = useRef<HTMLDivElement>(null);
|
||
const descriptionRef = useRef<HTMLParagraphElement>(null);
|
||
const imgRef = useRef<HTMLImageElement>(null);
|
||
|
||
useEffect(() => {
|
||
if (!imgRef.current) return;
|
||
imgRef.current!.onload = () =>
|
||
setImgHeight(imgRef.current?.clientHeight ?? 0);
|
||
}, [imgRef, expanded]);
|
||
|
||
useEffect(() => {
|
||
setDescriptionHeight(descriptionRef.current?.clientHeight ?? 0);
|
||
}, [descriptionRef, expanded]);
|
||
|
||
useOnClickOutside(root, () => setExpanded(false), 'mouseup');
|
||
|
||
return (
|
||
<motion.div
|
||
animate={{
|
||
height: expanded
|
||
? (root.current?.clientHeight ?? 0) +
|
||
descriptionHeight +
|
||
imgHeight +
|
||
56
|
||
: 72,
|
||
}}
|
||
transition={{ duration: 0.3 }}
|
||
ref={root}
|
||
onClick={() => setExpanded(prev => !prev)}
|
||
className="py-4 space-y-6 border-t last:border-b border-[#3D425C] relative select-none"
|
||
>
|
||
<div className="flex items-center justify-between py-2">
|
||
<p className="font-medium h3">{title}</p>
|
||
{expanded ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
||
</div>
|
||
<AnimatePresence>
|
||
{expanded && (
|
||
<motion.p
|
||
ref={descriptionRef}
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: +expanded, transition: { delay: 0.3 } }}
|
||
exit={{ opacity: 0 }}
|
||
transition={{ duration: 0.3 }}
|
||
className="mb-4 space-y-4 h4"
|
||
>
|
||
{description.map(paragraph => (
|
||
<p key={paragraph}>{paragraph}</p>
|
||
))}
|
||
</motion.p>
|
||
)}
|
||
</AnimatePresence>
|
||
<AnimatePresence>
|
||
{expanded && (
|
||
<motion.img
|
||
ref={imgRef}
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: +expanded, transition: { delay: 0.3 } }}
|
||
exit={{ opacity: 0 }}
|
||
transition={{ duration: 0.3 }}
|
||
src={img}
|
||
alt={title}
|
||
/>
|
||
)}
|
||
</AnimatePresence>
|
||
</motion.div>
|
||
);
|
||
}
|