form, modal, ellipse, devices, etc

This commit is contained in:
2024-09-03 14:35:39 +05:00
parent 0bd4038e05
commit b8be47299e
39 changed files with 509 additions and 165 deletions
+2 -1
View File
@@ -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",
-19
View File
@@ -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 />
</>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 927 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1011 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

+89
View File
@@ -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>
);
}
+4 -1
View File
@@ -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>
+157 -111
View File
@@ -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>
);
}
+44
View File
@@ -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>
+15
View File
@@ -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;
}
+25
View File
@@ -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>
);
}
+11 -3
View File
@@ -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>
);
+7 -4
View File
@@ -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>
+1 -1
View File
@@ -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 -2
View File
@@ -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>
);
+19
View File
@@ -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>
);
}
+52
View File
@@ -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',
},
];
+4 -4
View File
@@ -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 -1
View File
@@ -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 -2
View File
@@ -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>
);
}
+12
View File
@@ -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 }),
}));
+5
View File
@@ -0,0 +1,5 @@
export interface IDevice {
title: string;
description: string[];
img: string;
}
+12
View File
@@ -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"