form, modal, ellipse, devices, etc
@@ -17,7 +17,8 @@
|
||||
"react-input-mask": "^2.0.4",
|
||||
"react-router-dom": "^6.26.1",
|
||||
"react-swipeable": "^7.0.1",
|
||||
"usehooks-ts": "^3.1.0"
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"zustand": "^4.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.0",
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Footer } from './Footer';
|
||||
import { Header } from './Header';
|
||||
import { Feedback } from '../components/Feedback';
|
||||
import { Clients } from '../components/Clients';
|
||||
|
||||
export function Layout() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="lg:px-6">
|
||||
<Outlet />
|
||||
</main>
|
||||
<Clients />
|
||||
<Feedback />
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 526 KiB |
|
After Width: | Height: | Size: 705 KiB |
|
After Width: | Height: | Size: 711 KiB |
|
After Width: | Height: | Size: 927 KiB |
|
After Width: | Height: | Size: 855 KiB |
|
After Width: | Height: | Size: 1011 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 90 KiB |
@@ -0,0 +1,89 @@
|
||||
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 } from 'usehooks-ts';
|
||||
|
||||
export function Devices() {
|
||||
return (
|
||||
<div id="devices" className="space-y-20">
|
||||
<Title>
|
||||
Работаем с
|
||||
<span className="text-gradient"> любыми типами оборудования</span>
|
||||
<br /> и предложим лучшее мультимедийное оснащение
|
||||
</Title>
|
||||
<div>
|
||||
{devices.map((device, index) => (
|
||||
<Device key={device.title} {...device} number={index + 1} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Device({
|
||||
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%)]"
|
||||
animate={{
|
||||
height: hovered
|
||||
? +(root.current?.clientHeight ?? 0) + descriptionHeight + 24
|
||||
: 112,
|
||||
}}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<p className="h3 font-medium">{title}</p>
|
||||
{hovered && (
|
||||
<motion.p
|
||||
ref={descriptionRef}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 0.6 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="l-text space-y-4 max-w-[calc(600/1552*100%)] absolute"
|
||||
>
|
||||
{description.map(paragraph => (
|
||||
<p>
|
||||
{paragraph}
|
||||
<br />
|
||||
</p>
|
||||
))}
|
||||
</motion.p>
|
||||
)}
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{hovered && (
|
||||
<motion.img
|
||||
src={img}
|
||||
alt={title}
|
||||
className="absolute bottom-0 right-[calc(144/1552*100%)] w-[calc(560/1552*100%)]"
|
||||
initial={{ opacity: 0, y: 500 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 100 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<p className="m-text text-[#52587A] font-medium">[0{number}]</p>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,10 @@ import { Link } from 'react-router-dom';
|
||||
|
||||
export function Feedback() {
|
||||
return (
|
||||
<div className="grid grid-cols-12 grid-rows-[repeat(min-content,2)] gap-x-4 gap-y-20 lg:px-6 pb-20 mt-[200px]">
|
||||
<div
|
||||
id="contacts"
|
||||
className="grid grid-cols-12 grid-rows-[repeat(min-content,2)] gap-x-4 gap-y-20 lg:px-6 pb-20 mt-[200px]"
|
||||
>
|
||||
<h2 className="col-span-7 h2 font-medium">
|
||||
Хотите увеличить конверсию?{' '}
|
||||
<span className="text-gradient">Давайте обсудим детали.</span>
|
||||
|
||||
@@ -60,131 +60,177 @@ export function Form() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-[180px] py-6 space-y-20">
|
||||
<div className="py-6 space-y-20">
|
||||
<Title className="max-w-[58vw]">
|
||||
Хотите интерактивное решение для выставки?
|
||||
<br />
|
||||
<span className="text-gradient">Давайте обсудим детали.</span>
|
||||
</Title>
|
||||
<form onSubmit={handleSubmit} className="space-y-12 max-w-[62vw]">
|
||||
<div className="space-y-6">
|
||||
<div className="flex gap-x-4 items-start">
|
||||
<div className="w-full">
|
||||
<label
|
||||
className="m-text text-[#9299BD] select-none"
|
||||
htmlFor="name"
|
||||
>
|
||||
Имя
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
id="name"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder="Ваше имя"
|
||||
className="bg-transparent border-b border-[#3D425C] focus:border-white py-4 rounded-none outline-none transition-all w-full placeholder:h4 placeholder:font-medium placeholder:select-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
<label
|
||||
className="m-text text-[#9299BD] select-none"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email*
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
id="email"
|
||||
type="text"
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
placeholder="Ваш email"
|
||||
className="bg-transparent border-b border-[#3D425C] focus:border-white py-4 rounded-none outline-none transition-all w-full placeholder:h4 placeholder:font-medium placeholder:select-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
<label
|
||||
className="m-text text-[#9299BD] select-none"
|
||||
htmlFor="tel"
|
||||
>
|
||||
Телефон
|
||||
</label>
|
||||
<div className="flex gap-x-3 py-4 border-[#3D425C] relative">
|
||||
<SelectPhoneCode
|
||||
currentPhoneCode={phoneCode}
|
||||
onClick={setPhoneCode}
|
||||
/>
|
||||
<div className="border-l border-[#3D425C]" />
|
||||
<ReactInputMask
|
||||
<div className="relative">
|
||||
<form onSubmit={handleSubmit} className="space-y-12 max-w-[66vw]">
|
||||
<div className="space-y-6">
|
||||
<div className="flex gap-x-4 items-start">
|
||||
<div className="w-full">
|
||||
<label
|
||||
className="m-text text-[#9299BD] select-none"
|
||||
htmlFor="name"
|
||||
>
|
||||
Имя
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
type="tel"
|
||||
id="tel"
|
||||
mask={'(999) 99 999 99'}
|
||||
maskChar={null}
|
||||
value={phone}
|
||||
placeholder="(900) 000 00 00"
|
||||
onChange={e => setPhone(e.target.value)}
|
||||
className="bg-transparent rounded-none outline-none transition-all w-full placeholder:h4 placeholder:font-medium placeholder:select-none peer"
|
||||
id="name"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder="Ваше имя"
|
||||
className="bg-transparent border-b border-[#3D425C] focus:border-white py-4 rounded-none outline-none transition-all w-full placeholder:h4 placeholder:font-medium placeholder:select-none"
|
||||
/>
|
||||
<div className="bottom-0 absolute w-full border-b border-[#3D425C] peer-focus:border-white -mb-px" />
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
<label
|
||||
className="m-text text-[#9299BD] select-none"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email*
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
id="email"
|
||||
type="text"
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
placeholder="Ваш email"
|
||||
className="bg-transparent border-b border-[#3D425C] focus:border-white py-4 rounded-none outline-none transition-all w-full placeholder:h4 placeholder:font-medium placeholder:select-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
<label
|
||||
className="m-text text-[#9299BD] select-none"
|
||||
htmlFor="tel"
|
||||
>
|
||||
Телефон
|
||||
</label>
|
||||
<div className="flex gap-x-3 py-4 border-[#3D425C] relative">
|
||||
<SelectPhoneCode
|
||||
currentPhoneCode={phoneCode}
|
||||
onClick={setPhoneCode}
|
||||
/>
|
||||
<div className="border-l border-[#3D425C]" />
|
||||
<ReactInputMask
|
||||
required
|
||||
type="tel"
|
||||
id="tel"
|
||||
mask={'(999) 99 999 99'}
|
||||
maskChar={null}
|
||||
value={phone}
|
||||
placeholder="(900) 000 00 00"
|
||||
onChange={e => setPhone(e.target.value)}
|
||||
className="bg-transparent rounded-none outline-none transition-all w-full placeholder:h4 placeholder:font-medium placeholder:select-none peer"
|
||||
/>
|
||||
<div className="bottom-0 absolute w-full border-b border-[#3D425C] peer-focus:border-white -mb-px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
className="m-text text-[#9299BD] select-none"
|
||||
htmlFor="description"
|
||||
>
|
||||
Задача
|
||||
</label>
|
||||
<textarea
|
||||
ref={textAreaRef}
|
||||
id="description"
|
||||
placeholder="Опишите вашу задачу"
|
||||
value={description}
|
||||
rows={1}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
className="bg-transparent border-b py-4 focus:border-white max-h-[300px] h-auto rounded-none border-[#3D425C] resize-none outline-none transition-all w-full placeholder:h4 placeholder:font-medium placeholder:select-none focus:overflow-y-scroll overflow-hidden"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
className="m-text text-[#9299BD] select-none"
|
||||
htmlFor="description"
|
||||
>
|
||||
Задача
|
||||
</label>
|
||||
<textarea
|
||||
ref={textAreaRef}
|
||||
id="description"
|
||||
placeholder="Опишите вашу задачу"
|
||||
value={description}
|
||||
rows={1}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
className="bg-transparent border-b py-4 focus:border-white max-h-[300px] h-auto rounded-none border-[#3D425C] resize-none outline-none transition-all w-full placeholder:h4 placeholder:font-medium placeholder:select-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 max-w-[23vw]">
|
||||
<Button
|
||||
width="full"
|
||||
disabled={isLoading}
|
||||
className="p-2 pl-8"
|
||||
icon={
|
||||
isLoading ? (
|
||||
<ClassNameWrapper
|
||||
element={<LoaderIcon />}
|
||||
className="relative w-5 h-5 animate-spin"
|
||||
/>
|
||||
) : (
|
||||
<div className="p-2 rounded-full bg-white">
|
||||
<div className="space-y-4 max-w-[23vw]">
|
||||
<Button
|
||||
width="full"
|
||||
disabled={isLoading}
|
||||
className="p-2 pl-8"
|
||||
icon={
|
||||
isLoading ? (
|
||||
<ClassNameWrapper
|
||||
element={<ArrowRightIcon />}
|
||||
className="w-5 h-5 text-black"
|
||||
element={<LoaderIcon />}
|
||||
className="relative w-5 h-5 animate-spin"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
Отправить
|
||||
</Button>
|
||||
<p className="m-text text-[#52587A]">
|
||||
*нажимая кнопку отправить, вы принимаете
|
||||
<span className="text-gradient">
|
||||
{' '}
|
||||
условия использования и политику конфиденциальности
|
||||
</span>
|
||||
</p>
|
||||
) : (
|
||||
<div className="p-2 rounded-full bg-white">
|
||||
<ClassNameWrapper
|
||||
element={<ArrowRightIcon />}
|
||||
className="w-5 h-5 text-black"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
Отправить
|
||||
</Button>
|
||||
<p className="m-text text-[#52587A]">
|
||||
*нажимая кнопку отправить, вы принимаете
|
||||
<span className="text-gradient">
|
||||
{' '}
|
||||
условия использования и политику конфиденциальности
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
<div className="absolute bottom-0 -right-[min(136px,calc(136/1600*100vw))] animate-[spin_5s_linear_infinite]">
|
||||
<div className="relative w-[calc(512/1600*100vw)] aspect-square flex justify-center transition-all duration-700 hover:w-[calc(446/1600*100vw)] origin-center">
|
||||
<div className="w-[calc(116/1600*100vw)] flex flex-col justify-between h-full absolute">
|
||||
<img src="/src/assets/form/1_1.png" alt="" />
|
||||
<img src="/src/assets/form/1_2.png" alt="" />
|
||||
</div>
|
||||
<div className="w-[calc(116/1600*100vw)] flex flex-col justify-between h-full absolute rotate-45">
|
||||
<img
|
||||
src="/src/assets/form/2_1.png"
|
||||
className="-rotate-45"
|
||||
alt=""
|
||||
/>
|
||||
<img
|
||||
src="/src/assets/form/2_2.png"
|
||||
className="-rotate-45"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div className="w-[calc(116/1600*100vw)] flex flex-col justify-between h-full absolute rotate-90">
|
||||
<img
|
||||
src="/src/assets/form/3_1.png"
|
||||
className="-rotate-90"
|
||||
alt=""
|
||||
/>
|
||||
<img
|
||||
src="/src/assets/form/3_2.png"
|
||||
className="-rotate-90"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div className="w-[calc(116/1600*100vw)] flex flex-col justify-between h-full absolute rotate-[135deg]">
|
||||
<img
|
||||
src="/src/assets/form/4_1.png"
|
||||
className="-rotate-[135deg]"
|
||||
alt=""
|
||||
/>
|
||||
<img
|
||||
src="/src/assets/form/4_2.png"
|
||||
className="-rotate-[135deg]"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
export function Ellipse() {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const requestRef = useRef<number>();
|
||||
const x = useRef(0);
|
||||
const y = useRef(0);
|
||||
const [mousePos, setMousePos] = useState([0, 0]);
|
||||
|
||||
function handleMouseMove(e: MouseEvent) {
|
||||
x.current =
|
||||
e.clientX - (e.currentTarget as HTMLElement).getBoundingClientRect().left;
|
||||
y.current =
|
||||
e.clientY - (e.currentTarget as HTMLElement).getBoundingClientRect().top;
|
||||
}
|
||||
|
||||
function animate() {
|
||||
if (ref.current) {
|
||||
setMousePos(([prevX, prevY]) => [
|
||||
prevX + (x.current - prevX) / 15,
|
||||
prevY + (y.current - prevY) / 15,
|
||||
]);
|
||||
}
|
||||
requestRef.current = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
document.body?.addEventListener('mousemove', handleMouseMove);
|
||||
animate();
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(requestRef.current!);
|
||||
document.body?.removeEventListener('mousemove', handleMouseMove);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
style={{ top: mousePos[1], left: mousePos[0] }}
|
||||
className="absolute -z-10 bg-[url('src/assets/Ellipse.png')] bg-cover bg-no-repeat bg-center -translate-y-[75%] -translate-x-[50%] aspect-[348.75/262.77] w-[21.75vw]"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Logo } from '../components/icons/Logo';
|
||||
import { Logo } from '../icons/Logo';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="sm:grid xl:grid-cols-[2fr_1fr_1fr] sm:grid-cols-2 sm:max-xl:grid-rows-2">
|
||||
<footer className="sm:grid xl:grid-cols-[2fr_1fr_1fr] sm:grid-cols-2 sm:max-xl:grid-rows-2 bg-[#14161F]">
|
||||
<div className="flex sm:items-center max-sm:flex-col sm:px-6 px-4 sm:py-9 py-4 border-t border-[#3D425C] gap-6 sm:max-xl:row-start-1 sm:max-xl:col-span-2">
|
||||
<Link to={'/'}>
|
||||
<Logo />
|
||||
@@ -1,11 +1,15 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Logo } from '../components/icons/Logo';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { ClassNameWrapper } from '../hocs/ClassNameWrapper';
|
||||
import { Logo } from '../icons/Logo';
|
||||
import { Button } from '../ui/Button';
|
||||
import { ClassNameWrapper } from '../../hocs/ClassNameWrapper';
|
||||
import { useModalStore } from '../../stores/modalStore';
|
||||
import { ModalWithForm } from './ModalWithForm';
|
||||
|
||||
export function Header() {
|
||||
const { setModal } = useModalStore();
|
||||
|
||||
return (
|
||||
<header className="lg:px-6 flex items-center h-16 border-b border-[#3D425C]">
|
||||
<header className="lg:px-6 flex items-center h-16 border-b border-[#3D425C] bg-[#14161F]">
|
||||
<Link to={'/'}>
|
||||
<ClassNameWrapper element={<Logo />} className="h-10" />
|
||||
</Link>
|
||||
@@ -19,7 +23,10 @@ export function Header() {
|
||||
<HashLink key={link.path} {...link} />
|
||||
))}
|
||||
</nav>
|
||||
<Button className="-mr-6 rounded-none h-full px-10">
|
||||
<Button
|
||||
className="-mr-6 rounded-none h-full px-10"
|
||||
onClick={() => setModal(<ModalWithForm />)}
|
||||
>
|
||||
Отправить заявку
|
||||
</Button>
|
||||
</header>
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useModalStore } from '../../stores/modalStore';
|
||||
|
||||
export function ModalContainer() {
|
||||
const modal = useModalStore(state => state.modal);
|
||||
|
||||
return (
|
||||
modal && (
|
||||
<div className="fixed top-0 left-0 z-50 w-full h-full flex justify-center items-center bg-black bg-opacity-40 transition-opacity">
|
||||
<div onClick={e => e.stopPropagation()} className="cursor-default">
|
||||
{modal}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { api } from '../api';
|
||||
import { phoneCodes } from '../consts/phoneCodes';
|
||||
import { ClassNameWrapper } from '../hocs/ClassNameWrapper';
|
||||
import { useModalStore } from '@/stores/useModalStore';
|
||||
import { PhoneCode } from '@/types/PhoneCode';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { api } from '../../api';
|
||||
import { phoneCodes } from '../../consts/phoneCodes';
|
||||
import { ClassNameWrapper } from '../../hocs/ClassNameWrapper';
|
||||
import { useModalStore } from '../../stores/modalStore';
|
||||
import { PhoneCode } from '../../types/PhoneCode';
|
||||
import { Button } from '../ui/Button';
|
||||
import { FormEvent, useEffect, useRef, useState } from 'react';
|
||||
import ReactInputMask from 'react-input-mask';
|
||||
import { ArrowRightIcon } from '../icons/ArrowRightIcon';
|
||||
@@ -67,7 +67,7 @@ export function ModalWithForm() {
|
||||
useEffect(() => {
|
||||
const listener = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
setModal(false, 'form');
|
||||
setModal(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', listener);
|
||||
@@ -81,7 +81,7 @@ export function ModalWithForm() {
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="font-medium accent">Оставьте заявку</p>
|
||||
<button
|
||||
onClick={() => setModal(null, '')}
|
||||
onClick={() => setModal(null)}
|
||||
className="p-2 lg:hover:bg-white lg:hover:bg-opacity-10 transition-colors rounded-full"
|
||||
>
|
||||
<CloseIcon />
|
||||
@@ -221,7 +221,7 @@ export function ModalWithForm() {
|
||||
element={<ArrowRightIcon />}
|
||||
/>
|
||||
}
|
||||
onClick={() => setModal(false, '')}
|
||||
onClick={() => setModal(false)}
|
||||
>
|
||||
На главную
|
||||
</Button>
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
export function ScrollToHashElement() {
|
||||
const { hash } = useLocation();
|
||||
|
||||
const hashElement = document.getElementById(hash.slice(1));
|
||||
|
||||
useEffect(() => {
|
||||
if (hashElement)
|
||||
hashElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
inline: 'nearest',
|
||||
});
|
||||
}, [hashElement]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Footer } from './Footer';
|
||||
import { Header } from './Header';
|
||||
import { Feedback } from '../Feedback';
|
||||
import { Clients } from '../Clients';
|
||||
import { ScrollToHashElement } from './ScrollToHashElement';
|
||||
import { ModalContainer } from './ModalContainer';
|
||||
import { Ellipse } from './Ellipse';
|
||||
|
||||
export function Layout() {
|
||||
return (
|
||||
<div className="overflow-clip">
|
||||
<ScrollToHashElement />
|
||||
<Header />
|
||||
<main className="lg:px-6 relative">
|
||||
<Ellipse />
|
||||
<Outlet />
|
||||
</main>
|
||||
<Clients />
|
||||
<Feedback />
|
||||
<Footer />
|
||||
<ModalContainer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,12 +4,20 @@ import { Title } from './ui/Title';
|
||||
|
||||
export function Projects() {
|
||||
return (
|
||||
<div id={'/projects'} className="space-y-6 mt-[180px]">
|
||||
<div id={'projects'} className="space-y-6">
|
||||
<Title>Проекты</Title>
|
||||
<div className="flex flex-col gap-y-16">
|
||||
<div className="flex gap-x-4">{projects.slice(0, 2).map(Project)}</div>
|
||||
<div className="flex gap-x-4">
|
||||
{projects.slice(0, 2).map(project => (
|
||||
<Project {...project} key={project.title} />
|
||||
))}
|
||||
</div>
|
||||
<Project {...projects[2]} className="max-w-[82vw] self-end" />
|
||||
<div className="flex gap-x-4">{projects.slice(3).map(Project)}</div>
|
||||
<div className="flex gap-x-4">
|
||||
{projects.slice(3).map(project => (
|
||||
<Project {...project} key={project.title} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useHover } from 'usehooks-ts';
|
||||
|
||||
export function Promotion() {
|
||||
return (
|
||||
<div className="pt-[100px] space-y-20">
|
||||
<div id="products" className="space-y-20 -mt-20">
|
||||
<Title className="max-w-[calc(1310/1600*100vw)]">
|
||||
Повышаем количество посетителей на стенде,
|
||||
<span className="text-gradient">
|
||||
@@ -43,7 +43,7 @@ function Feature({
|
||||
className="p-7 border-t border-x border-[#3D425C] last:border-b group"
|
||||
>
|
||||
<motion.div
|
||||
className="flex justify-between gap-x-4 overflow-hidden"
|
||||
className="flex justify-between gap-x-4 [clip-path:polygon(0%_0%,100%_0%,100%_calc(100%+28px),0%_calc(100%+28px))]"
|
||||
transition={{ duration: 0.7 }}
|
||||
animate={hovered ? { height: 424 } : { height: 140 }}
|
||||
>
|
||||
@@ -56,8 +56,11 @@ function Feature({
|
||||
</div>
|
||||
<motion.p
|
||||
className="h4 opacity-60 font-medium"
|
||||
transition={{ delay: 0.5 }}
|
||||
animate={hovered ? { opacity: 0.6 } : { opacity: 0 }}
|
||||
animate={
|
||||
hovered
|
||||
? { opacity: 0.6, transition: { delay: 0.5 } }
|
||||
: { opacity: 0 }
|
||||
}
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Title } from './ui/Title';
|
||||
|
||||
export function Stands() {
|
||||
return (
|
||||
<div className="space-y-20 mt-[180px]">
|
||||
<div className="space-y-20">
|
||||
<Title>
|
||||
Мы разработчики с собственной
|
||||
<span className="text-gradient">
|
||||
|
||||
@@ -4,14 +4,16 @@ import { useEffect, useRef } from 'react';
|
||||
|
||||
export function Statistics() {
|
||||
return (
|
||||
<div className="mt-[180px] space-y-20 border-b border-[#3D425C]">
|
||||
<div className="space-y-20 border-b border-[#3D425C]">
|
||||
<Title>
|
||||
За 15 лет работы cоздали более <br />
|
||||
<span className="text-gradient"> 250 интерактивных проектов </span>с 3D
|
||||
графикой
|
||||
</Title>
|
||||
<div className="py-24 flex gap-x-4 items-end">
|
||||
{statistics.map(Figure)}
|
||||
{statistics.map(stat => (
|
||||
<Figure key={stat.title} {...stat} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
export function CloseIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12.0002 11.9999L17.6572 6.34331M12.0002 11.9999L6.34337 6.34302M12.0002 11.9999L17.6571 17.6567M12.0002 11.9999L6.34326 17.6568"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { IDevice } from '../types/IDevice';
|
||||
|
||||
export const devices: IDevice[] = [
|
||||
{
|
||||
title: 'Интерактивные экраны',
|
||||
description: [
|
||||
'За счет высокого разрешение, яркости и широкого угла обзора светодиодные стены четко и ярко отображают контент.',
|
||||
'Их используют на выставках, мероприятиях и в коммерческих пространствах, где нужно выделиться и оставить яркое впечатление.',
|
||||
],
|
||||
img: '/src/assets/devices/interactive.png',
|
||||
},
|
||||
{
|
||||
title: 'Светодиодные стены',
|
||||
description: [
|
||||
'За счет высокого разрешение, яркости и широкого угла обзора светодиодные стены четко и ярко отображают контент.',
|
||||
'Их используют на выставках, мероприятиях и в коммерческих пространствах, где нужно выделиться и оставить яркое впечатление.',
|
||||
],
|
||||
img: '/src/assets/devices/led.png',
|
||||
},
|
||||
{
|
||||
title: 'Проекционные экраны',
|
||||
description: [
|
||||
'За счет высокого разрешение, яркости и широкого угла обзора светодиодные стены четко и ярко отображают контент.',
|
||||
'Их используют на выставках, мероприятиях и в коммерческих пространствах, где нужно выделиться и оставить яркое впечатление.',
|
||||
],
|
||||
img: '/src/assets/devices/projection.png',
|
||||
},
|
||||
{
|
||||
title: 'Транспарентные экраны',
|
||||
description: [
|
||||
'За счет высокого разрешение, яркости и широкого угла обзора светодиодные стены четко и ярко отображают контент.',
|
||||
'Их используют на выставках, мероприятиях и в коммерческих пространствах, где нужно выделиться и оставить яркое впечатление.',
|
||||
],
|
||||
img: '/src/assets/devices/transparent.png',
|
||||
},
|
||||
{
|
||||
title: 'Голографические пирамиды',
|
||||
description: [
|
||||
'За счет высокого разрешение, яркости и широкого угла обзора светодиодные стены четко и ярко отображают контент.',
|
||||
'Их используют на выставках, мероприятиях и в коммерческих пространствах, где нужно выделиться и оставить яркое впечатление.',
|
||||
],
|
||||
img: '/src/assets/devices/holographic.png',
|
||||
},
|
||||
{
|
||||
title: 'Мобильные устройства',
|
||||
description: [
|
||||
'За счет высокого разрешение, яркости и широкого угла обзора светодиодные стены четко и ярко отображают контент.',
|
||||
'Их используют на выставках, мероприятиях и в коммерческих пространствах, где нужно выделиться и оставить яркое впечатление.',
|
||||
],
|
||||
img: '/src/assets/devices/mobile.png',
|
||||
},
|
||||
];
|
||||
@@ -43,9 +43,9 @@ body {
|
||||
/* leading-[clamp(17.6px,17.6px+(100vw-360px)/1240*6.4,24px)]; */;
|
||||
}
|
||||
|
||||
/* .accent {
|
||||
.accent {
|
||||
@apply -tracking-[.02em] md:text-[clamp(28px,28px+(100vw-768px)/832*4,32px)] text-[clamp(20px,20px+(100vw-360px)/408*8,28px)] md:leading-[clamp(28px,28px+(100vw-768px)/832*7.2,35.2px)] leading-[clamp(20px,20px+(100vw-360px)/408*8,28px)];
|
||||
} */
|
||||
}
|
||||
|
||||
.l-text {
|
||||
@apply text-[clamp(14px,14px+(100vw-360px)/1240*6,20px)] leading-[135%]
|
||||
@@ -59,11 +59,11 @@ body {
|
||||
|
||||
/* .l-caption {
|
||||
@apply text-[clamp(14px,14px+(100vw-360px)/1240*2,16px)] leading-none;
|
||||
}
|
||||
} */
|
||||
|
||||
.m-caption {
|
||||
@apply text-[clamp(10px,10px+(100vw-360px)/1240*2,12px)] leading-[clamp(12px,12px+(100vw-360px)/1240*2.4,14.4px)];
|
||||
} */
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
@apply -tracking-[.01em] text-[clamp(12px,12px+(100vw-360px)/1240*8,20px)] leading-none;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import './index.css';
|
||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||
import { Layout } from './Layout';
|
||||
import { Layout } from './components/Layout';
|
||||
import { MainPage } from './pages/MainPage';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
|
||||
@@ -4,16 +4,18 @@ import { Projects } from '../components/Projects';
|
||||
import { Promotion } from '../components/Promotion';
|
||||
import { Statistics } from '../components/Statistics';
|
||||
import { Form } from '../components/Form';
|
||||
import { Devices } from '../components/Devices';
|
||||
|
||||
export function MainPage() {
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-[180px]">
|
||||
<Motivation />
|
||||
<Promotion />
|
||||
<Projects />
|
||||
<Form />
|
||||
<Stands />
|
||||
<Statistics />
|
||||
</>
|
||||
<Devices />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface IModalState {
|
||||
modal: ReactNode | null;
|
||||
setModal: (modal: ReactNode) => void;
|
||||
}
|
||||
|
||||
export const useModalStore = create<IModalState>(set => ({
|
||||
modal: null,
|
||||
setModal: modal => set({ modal }),
|
||||
}));
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface IDevice {
|
||||
title: string;
|
||||
description: string[];
|
||||
img: string;
|
||||
}
|
||||
@@ -2028,6 +2028,11 @@ uri-js@^4.2.2:
|
||||
dependencies:
|
||||
punycode "^2.1.0"
|
||||
|
||||
use-sync-external-store@1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9"
|
||||
integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==
|
||||
|
||||
usehooks-ts@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/usehooks-ts/-/usehooks-ts-3.1.0.tgz#156119f36efc85f1b1952616c02580f140950eca"
|
||||
@@ -2102,3 +2107,10 @@ yocto-queue@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"
|
||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||
|
||||
zustand@^4.5.5:
|
||||
version "4.5.5"
|
||||
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.5.tgz#f8c713041543715ec81a2adda0610e1dc82d4ad1"
|
||||
integrity sha512-+0PALYNJNgK6hldkgDq2vLrw5f6g/jCInz52n9RTpropGgeAf/ioFUCdtsjCqu4gNhW9D01rUQBROoRjdzyn2Q==
|
||||
dependencies:
|
||||
use-sync-external-store "1.2.2"
|
||||
|
||||