fixes and updates

This commit is contained in:
2024-09-05 17:38:43 +05:00
parent 8ebb077cc4
commit cf2e539fa8
43 changed files with 684 additions and 334 deletions
+18 -4
View File
@@ -3,11 +3,25 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="keywords" content="интерактивные,тренажеры,симуляторы,эффективность,VR,промышленность,обучение,образование">
<meta
name="keywords"
content="интерактивные,тренажеры,симуляторы,эффективность,VR,промышленность,обучение,образование"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta property="og:title" content="Интерактивные тренажеры для промышленности и образования" />
<meta property="og:description" content="Создаем интерактивные тренажеры для промышленности и образования. Помогаем сократить затраты на обучение, повысить безопасность и производительность"/>
<meta property="og:url" content="https://graff.training"/>
<meta
property="og:title"
content="Интерактивные тренажеры для промышленности и образования"
/>
<meta
property="og:description"
content="Создаем интерактивные тренажеры для промышленности и образования. Помогаем сократить затраты на обучение, повысить безопасность и производительность"
/>
<meta property="og:url" content="https://graff.training" />
<meta
property="og:image"
content="https://graff.training/src/assets/decreasing/effect.jpg"
/>
<meta property="og:type" content="website" />
<title>Интерактивные тренажеры для промышленности и образования</title>
</head>
<body>
+3 -2
View File
@@ -10,16 +10,17 @@
"preview": "vite preview"
},
"dependencies": {
"countries-phone-masks": "^1.1.0",
"framer-motion": "^11.2.14",
"ky": "^1.4.0",
"libphonenumber-js": "^1.11.7",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-input-mask": "^2.0.4",
"react-phone-number-input": "^3.4.5",
"react-rangeslider": "^2.2.0",
"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"
},
+1 -1
View File
@@ -11,7 +11,7 @@ export function Footer() {
<div className="flex flex-col gap-y-1">
<Link
to="https://graff.tech/privacypolicy"
className="sm:font-medium flex gap-4 m-text outline-none"
className="flex gap-4 outline-none sm:font-medium m-text"
>
Политика конфиденциальности <span>graff.tech</span>
</Link>
+16 -15
View File
@@ -1,9 +1,8 @@
import { motion } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import { PropsWithChildren, useRef, useState } from 'react';
import { AnchorLink } from '../../ui/AnchorLink';
import { Link } from 'react-router-dom';
import { Lang, useLanguageStore } from '../../store/languageStore';
import { HashLink } from 'react-router-hash-link';
import { useHover, useOnClickOutside } from 'usehooks-ts';
import { useModalStore } from '../../store/modalStore';
import { ModalWithForm } from '../Main/ModalWithForm';
@@ -37,14 +36,14 @@ export function Header() {
<LogoIcon className="lg:hidden" />
{width >= 1024 && <LogoWithTextIcon />}
</Link>
<div className="flex">
<div className="flex mx-auto">
<AnchorLink route="#products">Типы тренажеров</AnchorLink>
<AnchorLink route="#trainings">Варианты комплектации</AnchorLink>
<AnchorLink route="#projects">Проекты</AnchorLink>
<AnchorLink route="#events">События</AnchorLink>
</div>
<Button
onClick={() => setModal(<ModalWithForm />)}
className="rounded-none btn-text font-semibold max-sm:hidden px-10"
className="px-10 font-semibold rounded-none btn-text max-sm:hidden"
>
Оставить заявку
</Button>
@@ -52,11 +51,10 @@ export function Header() {
<button
ref={menuBtnRef}
onClick={() => setMenuOpen(prev => !prev)}
className="px-6 py-5 min-[1350px]:hidden border-[#3D425C] max-sm:border-l outline-none"
className="px-6 py-5 xl:hidden border-[#3D425C] max-sm:border-l outline-none"
>
{!menuOpen ? <BurgerIcon /> : <CloseIcon />}
</button>
</div>
</nav>
<motion.div
initial={{ opacity: 0, visibility: 'hidden' }}
@@ -76,7 +74,6 @@ export function Header() {
<BurgerAnchor route="#products">Типы тренажеров</BurgerAnchor>
<BurgerAnchor route="#trainings">Варианты комплектации</BurgerAnchor>
<BurgerAnchor route="#projects">Проекты</BurgerAnchor>
<BurgerAnchor route="#events">События</BurgerAnchor>
</div>
<div className="grid grid-cols-[2fr_1fr_1fr] sm:grid-cols-2">
<Button
@@ -85,7 +82,7 @@ export function Header() {
setModal(<ModalWithForm />);
}}
width="full"
className="sm:hidden font-semibold btn-text rounded-none outline-none"
className="font-semibold rounded-none outline-none sm:hidden btn-text"
>
Оставить заявку
</Button>
@@ -102,13 +99,13 @@ function BurgerAnchor({
route,
}: PropsWithChildren<{ route: string }>) {
return (
<HashLink
<Link
to={route}
className="flex items-center px-10 py-6 gap-1 btn-text bg-[#14161F] w-full last:border-b-0 [&:not(:last-child)]:border-b sm:border-l font-semibold border-[#3D425C] lg:hover:bg-[#3D425C] active:bg-[#14161F]"
>
<CubeIcon />
{children}
</HashLink>
</Link>
);
}
@@ -138,28 +135,32 @@ function LangToggler({ lang }: { lang: Lang }) {
return (
<div
className="max-w-[101px] max-[1349px]:hidden"
className="max-w-[101px]s max-xl:hidden relative"
style={{ backgroundColor: hovered ? '#3D425C' : 'transparent' }}
>
<button
ref={langTogglerRef}
onClick={() => setOpen(prev => !prev)}
className="mx-6 h-full gap-x-1 items-center flex font-semibold btn-text outline-none"
className="flex items-center h-full mx-6 font-semibold outline-none gap-x-1 btn-text"
>
{lang}
<ChevronDownIcon />
</button>
<AnimatePresence>
<motion.div
className="absolute z-20 grid grid-cols-2 min-w-[101px]"
className="absolute z-20 grid grid-cols-2 min-w-[101px]s w-full"
onClick={() => setOpen(false)}
initial={{ visibility: 'hidden' }}
initial={{ visibility: 'hidden', opacity: 0 }}
animate={{
visibility: open ? 'visible' : 'hidden',
opacity: +open,
}}
exit={{ visibility: 'hidden', opacity: 0 }}
>
<ChooseLang currentLang={'RU'} />
<ChooseLang currentLang={'EN'} />
</motion.div>
</AnimatePresence>
</div>
);
}
+3 -1
View File
@@ -4,12 +4,14 @@ import { Footer } from './Footer';
import { ModalContainer } from '../Main/ModalContainer';
import { Feedback } from '../Main/Contacts';
import { Clients } from '../Main/Clients';
import { ScrollToHashElement } from './ScrollToHashElement';
export function Layout() {
return (
<>
<ScrollToHashElement />
<Header />
<main className="relative overflow-x-clip lg:px-10 sm:px-6 px-4">
<main className="relative px-4 overflow-clip lg:px-10 sm:px-6">
<Outlet />
<Clients />
<Feedback />
@@ -0,0 +1,19 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
export function ScrollToHashElement() {
const { hash } = useLocation();
useEffect(() => {
const hashElement = document.getElementById(hash.slice(1));
if (hashElement)
hashElement.scrollIntoView({
behavior: 'smooth',
inline: 'nearest',
block: 'start',
});
}, [hash]);
return null;
}
+15 -11
View File
@@ -7,13 +7,14 @@ import { getIcon } from '../../utils/getIcon';
export function Availables() {
return (
<div className="lg:py-[100px] py-14 sm:grid lg:grid-cols-12 lg:grid-rows-[repeat(4,fit)] max-lg:grid-cols-8">
<Title className="lg:mb-14 mb-6 col-span-full">
<div className="lg:py-[100px] py-14 sm:grid lg:grid-cols-12 sm:grid-rows-[repeat(4,fit)] sm:grid-cols-3">
<Title className="row-start-1 mb-6 lg:mb-14 col-span-full">
<span className="text-gradient text-wrap">Многопользовательский </span>
<br className="lg:hidden" />
режим обучения
</Title>
<div className="col-span-6 col-start-1 row-span-2 row-start-2 bg-[url(src/assets/availables/image.png)] bg-cover bg-no-repeat bg-center" />
<div className="lg:col-span-6 row-span-2 grid grid-cols-2 grid-rows-2 lg:-mr-10 sm:-mr-6 slg:aspect-[808/560]">
<div className="max-lg:hidden lg:col-span-6 col-start-1 lg:row-span-2 sm:col-span-3 lg:row-start-2 sm:row-start-3 sm:row-span-1 bg-[url(src/assets/availables/image.png)] bg-cover bg-no-repeat bg-center" />
<div className="grid max-lg:col-span-full sm:max-lg:row-start-2 lg:col-span-6 lg:row-span-2 lg:grid-cols-2 sm:grid-cols-3 lg:grid-rows-2 lg:-mr-10 max-lg:-mx-6">
<MultiUserFeature
type="processes"
text="отработка производственных процессов, в которых участвует группа людей"
@@ -27,8 +28,9 @@ export function Availables() {
text="координация действий между несколькими сотрудниками"
/>
</div>
<div className="lg:hidden lg:col-span-6 col-start-1 lg:row-span-2 sm:col-span-3 lg:row-start-2 sm:row-start-3 sm:row-span-1 bg-[url(src/assets/availables/image.png)] bg-cover bg-no-repeat bg-center sm:aspect-[728/356] aspect-[3/2] max-sm:-mx-6" />
<AppearanceText
className="col-start-1 lg:col-span-6 col-span-7 mt-8 max-lg:mt-6"
className="col-span-7 col-start-1 mt-8 lg:col-span-6 max-lg:mt-6"
splits={[
'В одном ',
'цифровом ',
@@ -56,18 +58,20 @@ function MultiUserFeature({
const ref = useRef<HTMLDivElement>(null);
const hovered = useHover(ref);
const isInView = useInView(ref, {
margin: `0px 0px -${window.innerHeight - (ref.current?.clientHeight ?? 0)}px`,
margin: `0px 0px ${(ref.current?.clientHeight ?? 0) - window.innerHeight}px`,
});
return (
<div
ref={ref}
className={
'bg-right-bottom bg-no-repeat flex flex-col bg-[url(src/assets/availables/highlight.png)] bg-[length:0%_0%] hover:bg-[length:100%_100%] transition-all min-h-[280px] duration-300 w-full justify-between items-start px-10 py-6 lg:border-t lg:first:border-r lg:last:border-b border-[#3D425C] lg:col-span-1 lg:last:col-span-2 lg:aspect-[403/280]lg:last:aspect-[808/280]'
}
className="max-sm:border-t max-sm:last:border-b sm:max-lg:border-y max-h-60 sm:max-lg:[&:not(:last-child)]:border-r bg-right-bottom bg-no-repeat flex flex-col bg-[url(src/assets/availables/highlight.png)] bg-[length:0%_0%] hover:bg-[length:100%_100%] transition-all min-h-[280px] duration-300 w-full justify-between items-start px-10 py-6 lg:border-t lg:first:border-r lg:last:border-b border-[#3D425C] col-span-1 lg:last:col-span-2"
>
{getIcon(type, hovered, 'mb-4 max-sm:hidden w-14 h-14')}
{getIcon(type, isInView, 'mb-4 sm:hidden w-14 h-14')}
<div className="max-lg:hidden">
{getIcon(type, hovered, 'mb-4 max-lg:hidden w-14 h-14')}
</div>
<div className="lg:hidden">
{getIcon(type, isInView, 'mb-4 lg:hidden w-14 h-14')}
</div>
<p className="l-text">{text}</p>
</div>
);
+11 -7
View File
@@ -2,12 +2,12 @@ import { clients } from '../../consts/clients';
export function Clients() {
return (
<div className="space-y-6">
<div className="flex items-center overflow-hidden w-screen sm:-mx-6 mt-10 min-h-[117px]">
<div className="space-y-8">
<div className="flex items-center overflow-hidden w-screen lg:-mx-10 -mx-6 mt-10 min-h-[117px]">
<MarqueeHalf items={clients.slice(0, clients.length / 3)} />
<MarqueeHalf items={clients.slice(0, clients.length / 3)} />
</div>
<div className="flex items-center overflow-hidden w-screen sm:-mx-6 min-h-[117px]">
<div className="flex items-center overflow-hidden w-screen lg:-mx-10 -mx-6 min-h-[117px]">
<MarqueeHalf
reversed
items={clients.slice(clients.length / 3, (2 * clients.length) / 3)}
@@ -17,7 +17,7 @@ export function Clients() {
items={clients.slice(clients.length / 3, (2 * clients.length) / 3)}
/>
</div>
<div className="border-b border-[#3D425C] flex items-center overflow-hidden w-screen sm:-mx-6 min-h-[117px] pb-16">
<div className="border-b border-[#3D425C] flex items-center overflow-hidden w-screen lg:-mx-10 -mx-6 min-h-[117px] pb-16">
<MarqueeHalf items={clients.slice(2 * (clients.length / 3))} />
<MarqueeHalf items={clients.slice(2 * (clients.length / 3))} />
</div>
@@ -35,17 +35,21 @@ function MarqueeHalf({
return (
<div
className={
'flex flex-nowrap overflow-clip items-center py-2 [flex:0_0_auto] animate-infinite-scroll ' +
(reversed ? '[animation-direction:reverse]' : '')
'flex flex-nowrap ' +
(reversed
? '[animation:infinite-scroll_45s_linear_infinite_reverse]'
: 'animate-infinite-scroll')
}
>
{items.map(client => (
<div className="border-l border-[#3D425C] w-[312px] h-[124px] flex justify-center items-center relative">
<img
key={client.src}
src={client.src}
alt={client.src}
className="max-w-full h-auto !relative object-covesr mx-12"
className="!relative"
/>
</div>
))}
</div>
);
+5 -5
View File
@@ -10,7 +10,7 @@ import { YouTubeIcon } from '../icons/YoutubeIcon';
export function Feedback() {
return (
<div className="sm:grid lg:grid-cols-12 sm:grid-cols-2 lg:grid-rows-[repeat(min-content,2)] sm:grid-rows-[repeat(min-content,3)] lg:gap-x-4 sm:gap-x-14 lg:gap-y-[68px] pb-20 pt-[70px]">
<h2 className="lg:col-span-7 sm:col-span-full h2 font-medium max-lg:mb-6">
<h2 className="font-medium lg:col-span-7 sm:col-span-full h2 max-lg:mb-6">
Хотите использовать интерактивные тренажеры в обучении?
<br />
<span className="text-gradient">Давайте обсудим детали.</span>
@@ -18,13 +18,13 @@ export function Feedback() {
<Button
color="primary"
icon={<SendIcon />}
className="lg:col-span-3 row-start-2 self-end px-6 py-4 sm:max-lg:mb-20 max-sm:mb-14"
className="self-end row-start-2 px-6 py-4 lg:col-span-3 sm:max-lg:mb-20 max-sm:mb-14"
width="full"
>
Оставить заявку
</Button>
<div className="space-y-3 lg:col-start-9 lg:col-span-4 sm:col-span-1 sm:col-start-1 max-sm:mb-8">
<h4 className="h4 font-medium mb-1">Свяжитесь с нами</h4>
<h4 className="mb-1 text-xl font-medium">Свяжитесь с нами</h4>
<Button
color="secondary"
className="py-4"
@@ -42,8 +42,8 @@ export function Feedback() {
<span className="btn-text opacity-80">Позвонить</span>
</Button>
</div>
<div className="space-y-4 lg:col-start-9 lg:col-span-4 col-start-2 lg:row-start-2 sm:row-start-3 lg:self-end sm:flex sm:flex-col sm:justify-between">
<h4 className="h4 font-medium">Социальные сети</h4>
<div className="col-start-2 space-y-4 lg:col-start-9 lg:col-span-4 lg:row-start-2 sm:row-start-3 lg:self-end sm:flex sm:flex-col sm:justify-between">
<h4 className="font-medium h4">Социальные сети</h4>
<div className="flex gap-x-2">
<Link
to={'https://www.youtube.com/@GRAFFtech'}
+1 -1
View File
@@ -40,7 +40,7 @@ function DecreasingOption({ text, number }: { text: string; number: number }) {
return (
<div className="group">
<AppearanceHr delay={number * 0.5} />
<div className="flex justify-between items-center py-5 gap-x-4">
<div className="flex items-center justify-between py-5 gap-x-4">
<Plus text={text} />
<Number number={number} />
</div>
+4 -4
View File
@@ -3,13 +3,13 @@ import { Title } from '../../ui/Title';
export function Distance() {
return (
<div className="lg:py-[100px] py-14">
<Title className="lg:mb-14 mb-6">
<Title className="mb-6 lg:mb-14">
Платформа GRAFF.training поволяет осуществлять
<span className="text-gradient"> дистанционное обучение</span> с любого
устройства
</Title>
<div className="sm:grid lg:grid-cols-12 gap-x-4 gap-y-6">
<p className="row-start-1 lg:col-start-1 col-span-6 max-sm:mb-6 l-text">
<p className="col-span-6 row-start-1 lg:col-start-1 max-sm:mb-6 l-text">
Обучающиеся будут получать доступ к системе с высоко детализированной
3D графикой для прохождения сценариев на любом устройстве, без
необходимости установки дополнительного ПО.
@@ -17,12 +17,12 @@ export function Distance() {
<img
src="src/assets/distance/datamining_2.jpg"
alt="дистанционное обучение с любого устройства"
className="row-start-2 lg:col-start-1 col-span-6 max-sm:mb-4 rounded-2xl self-stretch object-cover"
className="self-stretch object-cover col-span-6 row-start-2 lg:col-start-1 max-sm:mb-4 rounded-2xl"
/>
<img
src="src/assets/distance/datamining_1.jpg"
alt="дистанционное обучение с любого устройства"
className="row-start-2 lg:col-start-7 lg:col-span-6 col-span-3 rounded-2xl h-full self-stretch object-cover"
className="self-stretch object-cover h-full col-span-3 row-start-2 lg:col-start-7 lg:col-span-6 rounded-2xl"
/>
</div>
</div>
+5 -5
View File
@@ -84,17 +84,17 @@ function Figure({
<motion.div
ref={root}
initial={{
background: `bottom right / 50% url(src/assets/efficiency/${type}.png) no-repeat, bottom right / 0% url(src/assets/efficiency/efficiency_backlight.svg) no-repeat`,
background: `bottom right / 45% url(src/assets/efficiency/${type}.png) no-repeat, bottom right / 0% url(src/assets/efficiency/efficiency_backlight.svg) no-repeat`,
}}
whileHover={{
backgroundSize: '50%,100% 100%',
transition: { duration: 0.075 },
backgroundSize: '45%,100%',
transition: { duration: 0.125 },
}}
className="flex px-10 w-full pt-6 bg-no-repeat max-sm:aspect-[3/2] max-sm:border-t max-sm:last:border-b lg:aspect-[532.67/360] relative lg:border-y sm:max-lg:border-t sm:max-lg:last:border-b lg:border-r sm:first:border-r last:border-r-0 border-[#3D425C] sm:max-lg:last:col-span-2 sm:max-lg:row-start-1 sm:max-lg:last:row-start-2"
className="flex lg:px-10 px-6 w-full pt-6 bg-no-repeat min-h-[240px] max-sm:border-t max-sm:last:border-b lg:aspect-[532.67/360] relative lg:border-y sm:max-lg:border-t sm:max-lg:last:border-b lg:border-r sm:first:border-r last:border-r-0 border-[#3D425C] sm:max-lg:last:col-span-2 sm:max-lg:row-start-1 sm:max-lg:last:row-start-2"
>
<div className="flex flex-col justify-between pb-6 max-sm:max-w-[50vw]">
<h3 className="lg:font-medium l-text 2xl:max-w-[70%]">{title}</h3>
<h1 className="font-medium flex items-center md:text-[clamp(64px,64px+(100vw-768px)/832*32,96px)] md:leading-[clamp(57.6px,57.6px+(100vw-768px)/832*28.8,86.4px)] text-[64px] leading-[57.6px]">
<h1 className="font-medium flex lg:items-center items-end md:text-[clamp(64px,64px+(100vw-768px)/832*32,96px)] md:leading-[clamp(57.6px,57.6px+(100vw-768px)/832*28.8,86.4px)] text-[64px] leading-[57.6px]">
<span ref={figureRef}>{percents}</span>
<span className="md:text-[clamp(32px,32px+(100vw-768px)/832*32,64px)] md:leading-[clamp(32px,32px+(100vw-768px)/832*25.6,57.6px)] text-[32px] leading-8">
%
+4 -4
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
export function Ellipse() {
const ref = useRef<HTMLDivElement>(null);
@@ -14,7 +14,7 @@ export function Ellipse() {
e.clientY - (e.currentTarget as HTMLElement).getBoundingClientRect().top;
}
function animate() {
const animate = useCallback(() => {
if (ref.current) {
setMousePos(([prevX, prevY]) => [
prevX + (x.current - prevX) / 15,
@@ -22,7 +22,7 @@ export function Ellipse() {
]);
}
requestRef.current = requestAnimationFrame(animate);
}
}, []);
useEffect(() => {
document.body?.addEventListener('mousemove', handleMouseMove);
@@ -32,7 +32,7 @@ export function Ellipse() {
cancelAnimationFrame(requestRef.current!);
document.body?.removeEventListener('mousemove', handleMouseMove);
};
}, []);
}, [animate]);
return (
<div
+6 -6
View File
@@ -10,11 +10,11 @@ export function Events() {
return (
<div className="lg:py-[70px] sm:py-14" id="events">
<AppearanceHr className="max-lg:hidden" />
<div className="pt-5 gap-x-4 w-full sm:grid lg:grid-cols-12 grid-cols-8">
<MiniTitle text="события" className="max-sm:mb-2 col-span-2" />
<div className="w-full grid-cols-8 pt-5 gap-x-4 sm:grid lg:grid-cols-12">
<MiniTitle text="события" className="col-span-2 max-sm:mb-2" />
<div className="lg:col-span-9 col-span-full">
<AppearanceHr className="sm:hidden" delay={0.5} />
<div className="lg:py-7 sm:py-6 py-5 flex justify-between max-sm:flex-col items-start gap-x-4">
<div className="flex items-start justify-between py-5 lg:py-7 sm:py-6 max-sm:flex-col gap-x-4">
<div className="w-fit">
<EventTitle className="sm:mb-8 w-fit">
Макет кабины машиниста «Иволга» на выставке ВДНХ
@@ -35,12 +35,12 @@ export function Events() {
<LinkButton href="/" />
</div>
<AppearanceHr delay={1} />
<div className="py-7 flex max-sm:flex-col sm:items-center justify-between gap-x-4">
<div className="flex justify-between py-7 max-sm:flex-col sm:items-center gap-x-4">
<EventTitle>Победа на BuildUP 2023 в номинации IT</EventTitle>
<LinkButton href="/" />
</div>
<AppearanceHr delay={1.5} />
<div className="py-7 flex max-sm:flex-col sm:items-center justify-between gap-x-4">
<div className="flex justify-between py-7 max-sm:flex-col sm:items-center gap-x-4">
<EventTitle>
Транспортное и специальное тренажеростроение 2023
</EventTitle>
@@ -65,7 +65,7 @@ function LinkButton({ href }: { href: string }) {
const hovered = useHover(ref);
return (
<div className="w-fit self-start">
<div className="self-start w-fit">
<Link
to={href}
ref={ref}
+1 -1
View File
@@ -5,7 +5,7 @@ export function ModalContainer() {
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 className="fixed top-0 left-0 z-50 flex items-center justify-center w-full h-full transition-opacity bg-black bg-opacity-40">
<div onClick={e => e.stopPropagation()} className="cursor-default">
{modal}
</div>
+281 -59
View File
@@ -1,29 +1,39 @@
'use client';
import { FormEvent, useEffect, useRef, useState } from 'react';
import { FormEvent, useEffect, useMemo, useRef, useState } from 'react';
import ReactInputMask from 'react-input-mask';
import { ArrowRightIcon } from '../icons/ArrowRightIcon';
import { CloseIcon } from '../icons/CloseIcon';
import { LoaderIcon } from '../icons/LoaderIcon';
import { MailIcon } from '../icons/MailIcon';
import { phoneCodes } from '../../consts/phoneCodes';
import { api } from '../../api/contactsFormInstance';
import { Button } from '../../ui/Button';
import { PhoneCode } from '../../types/PhoneCode';
import { ChevronUpIcon } from '../icons/ChevronUpIcon';
import { ChevronDownIcon } from '../icons/ChevronDownIcon';
import { useModalStore } from '../../store/modalStore';
import { SelectPhoneCode } from './SelectPhoneCode';
import { Country } from 'react-phone-number-input';
import { getExampleNumber } from 'libphonenumber-js';
import examples from 'libphonenumber-js/mobile/examples';
export function ModalWithForm() {
const { setModal } = useModalStore();
const [name, setName] = useState('');
const [phoneCode, setPhoneCode] = useState<PhoneCode>('+7');
const [[phoneCode, country], setPhoneCodeAndCountry] = useState<
[string, Country]
>(['+7', 'RU']);
const [phone, setPhone] = useState('');
const [email, setEmail] = useState('');
const [description, setDescription] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [isSend, setIsSend] = useState(false);
const placeholder = useMemo(
() =>
getExampleNumber(country, examples)
?.formatInternational()
.split(' ')
.slice(1)
.join(' '),
[country],
);
const textAreaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
@@ -79,11 +89,11 @@ export function ModalWithForm() {
<div className="fixed flex flex-col gap-4 top-0 right-0 h-full sm:w-[408px] w-full bg-[#14161F] overflow-y-auto sm:p-8 p-6">
{!isSend ? (
<div className="space-y-8">
<div className="flex justify-between items-center">
<div className="flex items-center justify-between">
<p className="font-medium accent">Оставьте заявку</p>
<button
onClick={() => setModal(null)}
className="p-2 lg:hover:bg-white lg:hover:bg-opacity-10 transition-colors rounded-full"
className="p-2 transition-colors rounded-full lg:hover:bg-white lg:hover:bg-opacity-10"
>
<CloseIcon />
</button>
@@ -119,20 +129,20 @@ export function ModalWithForm() {
</label>
<div className="flex gap-x-3 py-2 border-[#3D425C] relative">
<SelectPhoneCode
currentPhoneCode={phoneCode}
onClick={setPhoneCode}
currentPhoneCodeAndCountry={[phoneCode, country]}
onClick={setPhoneCodeAndCountry}
/>
<div className="border-l border-[#3D425C]" />
<ReactInputMask
required
type="tel"
id="tel"
mask={'(999) 99 999 99'}
id={'tel'}
mask={placeholder?.replace(/\d/g, '9') ?? ''}
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"
placeholder={placeholder}
onChange={e => setPhone(e.target.value.replace(/ /g, ''))}
className="w-full transition-all bg-transparent rounded-none outline-none h4 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-2" />
</div>
@@ -201,7 +211,7 @@ export function ModalWithForm() {
) : (
<div className="">
<div className="space-y-8">
<h2 className="h2 font-medium">Спасибо за отправку заявки!</h2>
<h2 className="font-medium h2">Спасибо за отправку заявки!</h2>
<p className="m-text">
Мы ценим ваш интерес к нашей компании и в ближайшее время свяжемся
с вами для уточнения деталей проекта.
@@ -221,45 +231,257 @@ export function ModalWithForm() {
);
}
function SelectPhoneCode({
currentPhoneCode,
onClick,
}: {
currentPhoneCode: PhoneCode;
onClick: (phoneCode: PhoneCode) => void;
}) {
const [open, setOpen] = useState(false);
// function SelectPhoneCode({
// currentPhoneCode,
// onClick,
// }: {
// currentPhoneCode: PhoneCode;
// onClick: (phoneCode: PhoneCode) => void;
// }) {
// const [open, setOpen] = useState(false);
return (
<div className="relative flex flex-col">
<button
className="flex gap-x-4 items-center relative"
onClick={e => {
e.preventDefault();
setOpen(prev => !prev);
}}
>
<p className="h4">{currentPhoneCode}</p>
{open ? <ChevronUpIcon /> : <ChevronDownIcon />}
</button>
{open && (
<div className="absolute z-10 bg-[#14161F] top-[100%] w-[calc(100%+4px)] -left-1 border border-t-0 p-1 rounded-b-lg border-[#3D425C]">
{phoneCodes
.filter(phonecode => phonecode !== currentPhoneCode)
.map(phoneCode => (
<p
key={phoneCode}
className="h4 cursor-pointer hover:bg-[#3D425C] py-1"
onClick={() => {
onClick(phoneCode);
setOpen(false);
}}
>
{phoneCode}
</p>
))}
</div>
)}
</div>
);
}
// return (
// <div className="relative flex flex-col">
// <button
// className="relative flex items-center gap-x-4"
// onClick={e => {
// e.preventDefault();
// setOpen(prev => !prev);
// }}
// >
// <p className="h4">{currentPhoneCode}</p>
// {open ? <ChevronUpIcon /> : <ChevronDownIcon />}
// </button>
// {open && (
// <div className="absolute z-10 bg-[#14161F] top-[100%] w-[calc(100%+4px)] -left-1 border border-t-0 p-1 rounded-b-lg border-[#3D425C]">
// {phoneCodes
// .filter(phonecode => phonecode !== currentPhoneCode)
// .map(phoneCode => (
// <p
// key={phoneCode}
// className="h4 cursor-pointer hover:bg-[#3D425C] py-1"
// onClick={() => {
// onClick(phoneCode);
// setOpen(false);
// }}
// >
// {phoneCode}
// </p>
// ))}
// </div>
// )}
// </div>
// );
// }
// import ReactInputMask from 'react-input-mask';
// import { SelectPhoneCode } from './SelectPhoneCode';
// import { ClassNameWrapper } from '../../hocs/ClassNameWrapper';
// import { LoaderIcon } from '../icons/LoaderIcon';
// import { Button } from '../../ui/Button';
// import { ArrowRightIcon } from '../icons/ArrowRightIcon';
// import { FormEvent, useEffect, useMemo, useRef, useState } from 'react';
// import { Country } from 'react-phone-number-input';
// import { api } from '../../api/contactsFormInstance';
// import { getExampleNumber } from 'libphonenumber-js';
// import examples from 'libphonenumber-js/mobile/examples';
// export function ModalWithForm({
// inModal = true,
// send = () => {},
// }: {
// inModal?: boolean;
// send?: () => void;
// }) {
// const [name, setName] = useState('');
// const [[phoneCode, country], setPhoneCodeAndCountry] = useState<
// [string, Country]
// >(['+7', 'RU']);
// const [phone, setPhone] = useState('');
// const [email, setEmail] = useState('');
// const [description, setDescription] = useState('');
// const [isLoading, setIsLoading] = useState(false);
// const textAreaRef = useRef<HTMLTextAreaElement>(null);
// useEffect(() => {
// if (textAreaRef.current) {
// textAreaRef.current.style.height = 'auto';
// textAreaRef.current.style.height =
// textAreaRef.current.scrollHeight + 'px';
// }
// }, [textAreaRef, description]);
// function handleSubmit(e: FormEvent<HTMLFormElement>) {
// e.preventDefault();
// sendMail();
// }
// async function sendMail() {
// setIsLoading(true);
// try {
// await api
// .post('mail', {
// json: {
// fullname: name,
// phone: phoneCode + phone,
// email,
// request: description,
// },
// })
// .json();
// setIsLoading(false);
// send?.();
// } catch (error) {
// setIsLoading(false);
// if (error instanceof Error) {
// alert(error.message);
// }
// }
// }
// const placeholder = useMemo(
// () =>
// getExampleNumber(country, examples)
// ?.formatInternational()
// .split(' ')
// .slice(1)
// .join(' '),
// [country],
// );
// return (
// <form
// onSubmit={handleSubmit}
// className={
// inModal
// ? 'space-y-6'
// : 'lg:space-y-12 sm:space-y-36 space-y-8 lg:max-w-[66vw] sm:max-w-[calc(369/720*100%)]'
// }
// >
// <div className="space-y-6">
// <div
// className={
// 'grid gap-x-4 items-start ' +
// (inModal ? 'gap-y-6' : 'lg:grid-cols-3 max-lg:gap-y-4')
// }
// >
// <div className="w-full">
// <label
// className="m-text text-[#9299BD] select-none"
// htmlFor={'name' + +inModal}
// >
// Имя
// </label>
// <input
// required
// id={'name' + +inModal}
// 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' + +inModal}
// >
// Email*
// </label>
// <input
// required
// id={'email' + +inModal}
// 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' + +inModal}
// >
// Телефон
// </label>
// <div className="flex gap-x-3 py-4 border-[#3D425C] relative">
// <SelectPhoneCode
// currentPhoneCodeAndCountry={[phoneCode, country]}
// onClick={setPhoneCodeAndCountry}
// />
// <div className="border-l border-[#3D425C]" />
// <ReactInputMask
// required
// type="tel"
// id={'tel' + +inModal}
// mask={placeholder?.replace(/\d/g, '9') ?? ''}
// maskChar={null}
// value={phone}
// placeholder={placeholder}
// onChange={e => setPhone(e.target.value.replace(/ /g, ''))}
// className="w-full transition-all bg-transparent rounded-none outline-none h4 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' + +inModal}
// >
// Задача
// </label>
// <textarea
// ref={textAreaRef}
// id={'description' + +inModal}
// 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 className="space-y-4 lg:max-w-[25vw] sm:max-lg:mt-36">
// <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 bg-white rounded-full">
// <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>
// );
// }
+1 -4
View File
@@ -1,9 +1,7 @@
// import { Marquee } from '../Main/Marquee';
export function Motivation() {
return (
<div>
<div className="lg:py-28 sm:py-12 py-14 grid grid-cols-12">
<div className="grid grid-cols-12 lg:py-28 sm:py-12 py-14">
<h1 className="2xl:mb-[38px] pb-8 font-medium lg:block max-lg:hidden h1 col-span-full">
Создаем
<span className="text-gradient"> интерактивные тренажеры </span>
@@ -18,7 +16,6 @@ export function Motivation() {
производительность
</h3>
</div>
{/* <Marquee /> */}
</div>
);
}
+1 -1
View File
@@ -60,7 +60,7 @@ export function Products() {
return (
<div
id="products"
className="lg:pt-[100px] sm:max-lg:pt-[70px] max-sm:py-14 lg:-mx-10 sm:-mx-6 -mx-4 sm:space-y-[500px]"
className="lg:pt-[100px] sm:max-lg:pt-[70px] max-sm:py-14 lg:-mx-10 sm:-mx-6 -mx-4 sm:space-y-[500px] space-y-10"
>
<IndustrialTrainings ref={ref1} sticked={stiked1} />
<Simulators ref={ref2} sticked={stiked2} />
@@ -3,45 +3,13 @@ import { useHover } from 'usehooks-ts';
import { getIcon } from '../../../../utils/getIcon';
import { useInView } from 'framer-motion';
function ForTeachingOption({
title,
description,
type,
}: {
title: string;
description: string;
type: 'labs' | 'teaching';
}) {
const ref = useRef<HTMLDivElement>(null);
const hovered = useHover(ref);
const isInView = useInView(ref, {
margin: `0px 0px -${window.innerHeight - (ref.current?.clientHeight ?? 0)}px`,
});
return (
<div
ref={ref}
className="flex gap-x-7 items-start sm:max-lg:pr-3 max-sm:pb-4 max-sm:border-b border-[#3D425C]"
>
{getIcon(type, hovered, 'max-sm:hidden min-w-11')}
<div className="lg:pl-4 sm:pl-[13px] sm:border-l border-[#3D425C]">
<div className="flex items-center sm:items-start sm:max-lg:flex-col gap-x-2 mb-1">
{getIcon(type, hovered || isInView, 'sm:hidden min-w-11')}
<h4 className="font-medium lg:text-2xl sm:text-xl">{title}</h4>
</div>
<p className="opacity-60 l-text">{description}</p>
</div>
</div>
);
}
export const ForTeaching = forwardRef<HTMLDivElement>((_, ref) => {
return (
<div
ref={ref}
className="lg:ml-[129px] lg:min-h-[calc(100vh-276px)] lg:min-w-[calc(100vw-129px)] sm:min-h-[calc(100vh-176px)] min-h-[calc(100vh)] min-w-[100vw] sm:sticky z-50 lg:top-[276px] sm:top-[176px] top-0 lg:px-10 lg:pt-10 sm:px-6 sm:pt-6 px-4 pt-4 lg:border-l bg-[#14161F] border-t border-[#3D425C]"
>
<h2 className={'h2 font-medium flex justify-between'}>
<h2 className="flex justify-between font-medium h2">
Интерактивные тренажеры для учебных заведений
<p className="h3 font-medium text-[#3D425C]">03</p>
</h2>
@@ -59,7 +27,7 @@ export const ForTeaching = forwardRef<HTMLDivElement>((_, ref) => {
поломки оборудования, а также экономить на расходных средствах"
type="labs"
/>
<p className="lg:text-2xl sm:text-xl font-medium">
<p className="font-medium lg:text-2xl sm:text-xl">
Оснащение учебных классов и центров всем необходимым для
современного обучения под «ключ»
</p>
@@ -72,10 +40,42 @@ export const ForTeaching = forwardRef<HTMLDivElement>((_, ref) => {
/>
<img
src="src/assets/products/teaching/teaching_mobile.png"
className="sm:hidden"
className="mt-5 -mx-6 sm:hidden"
alt=""
/>
</div>
</div>
);
});
function ForTeachingOption({
title,
description,
type,
}: {
title: string;
description: string;
type: 'labs' | 'teaching';
}) {
const ref = useRef<HTMLDivElement>(null);
const hovered = useHover(ref);
const isInView = useInView(ref, {
margin: `0px 0px ${(ref.current?.clientHeight ?? 0) - window.innerHeight}px`,
});
return (
<div
ref={ref}
className="flex sm:gap-x-7 items-start sm:max-lg:pr-3 max-sm:pb-4 max-sm:border-b border-[#3D425C]"
>
{getIcon(type, hovered, 'max-sm:hidden min-w-11')}
<div className="lg:pl-4 sm:pl-[13px] sm:border-l border-[#3D425C]">
<div className="flex items-center mb-1 sm:items-start sm:max-lg:flex-col gap-x-2">
{getIcon(type, hovered || isInView, 'sm:hidden min-w-11')}
<h4 className="font-medium lg:text-2xl sm:text-xl">{title}</h4>
</div>
<p className="opacity-60 l-text">{description}</p>
</div>
</div>
);
}
@@ -25,7 +25,7 @@ function TeachingItem({
>
{getIcon(iconType, hovered, 'max-sm:hidden sm:max-lg:mb-[14px] min-w-11')}
<div className="sm:border-l border-[#3D425C] sm:pl-4">
<h4 className="flex items-center gap-x-2 font-medium lg:text-2xl sm:text-xl mb-2">
<h4 className="flex items-center mb-2 font-medium gap-x-2 lg:text-2xl sm:text-xl">
{getIcon(iconType, hovered || isInView, 'sm:hidden min-w-11')}
{title}
</h4>
@@ -42,9 +42,9 @@ export const IndustrialTrainings = forwardRef<
return (
<div
ref={ref}
className="sm:sticky top-0 min-h-[100svh] min-w-[100vw] overflow-hidden lg:px-10 lg:pt-10 sm:px-6 sm:pt-6 px-4 pt-4 border-t border-[#3D425C] bg-[#14161F] max-sm:flex flex-col items-center gap-y-4"
className="sm:sticky top-0 min-h-[100svh] min-w-[100vw] overflow-hiddens lg:px-10 lg:pt-10 sm:px-6 sm:pt-6 px-4 pt-4 border-t border-[#3D425C] bg-[#14161F] max-sm:flex flex-col items-center gap-y-4"
>
<div className="lg:space-y-14 sm:space-y-10 space-y-6">
<div className="space-y-6 lg:space-y-14 sm:space-y-10">
<h2
className={
'h2 font-medium w-full flex justify-between items-center' +
@@ -80,22 +80,22 @@ export const IndustrialTrainings = forwardRef<
<img
src="src/assets/products/trainings/trainings_desktop.png"
className={
'absolute right-0 top-[calc(121px)] object-cover lg:w-[calc(1000/1600*100vw)] xl:w-[calc(1152/1600*100vw)] max-lg:hidden' +
(sticked ? ' transition-opacity opacity-0' : ' opacity-100')
'absolute right-0 top-[121px] object-cover lg:w-[calc(1000/1600*100vw)] xl:w-[calc(1152/1600*100vw)] max-lg:hidden ' +
(sticked ? 'transition-opacity opacity-0' : 'opacity-100')
}
alt=""
/>
<img
src="src/assets/products/trainings/trainings_tablet.png"
className={
'absolute right-0 top-[120px] object-cover w-[calc(438/768*100vw)] hidden sm:max-lg:block' +
(sticked ? ' transition-opacity opacity-0' : ' opacity-100')
'absolute right-0 top-[120px] object-cover w-[calc(438/768*100vw)] hidden sm:max-lg:block ' +
(sticked ? 'transition-opacity opacity-0' : 'opacity-100')
}
alt=""
/>
<img
src="src/assets/products/trainings/trainings_mobile.png"
className="sm:hidden object-cover object-center"
className="object-cover object-center sm:hidden"
alt=""
/>
</div>
+8 -8
View File
@@ -36,7 +36,7 @@ import { IProject, Media } from '../../types/Project';
// </Title>
// <MiniTitle
// text="реализованные проекты"
// className="lg:ml-10 sm:ml-6 ml-4"
// className="ml-4 lg:ml-10 sm:ml-6"
// />
// <Slider projects={projects} />
// </div>
@@ -48,15 +48,15 @@ export const Project = forwardRef<HTMLDivElement, IProject<Media>>(
const [buffering, setBuffering] = useState(true);
return (
<div ref={ref} className="aspect-square flex flex-col relative">
<div ref={ref} className="relative flex flex-col aspect-square">
{media === Media.img ? (
<div
className="bg-cover bg-center bg-no-repeat flex-1"
className="flex-1 bg-center bg-no-repeat bg-cover"
style={{ backgroundImage: `url(${src})` }}
/>
) : (
<div
className="flex-1 overflow-hidden relative flex justify-center items-center bg-cover bg-center bg-no-repeat"
className="relative flex items-center justify-center flex-1 overflow-hidden bg-center bg-no-repeat bg-cover"
style={{ backgroundImage: `url(${src[1]})` }}
>
<video
@@ -74,7 +74,7 @@ export const Project = forwardRef<HTMLDivElement, IProject<Media>>(
<div className="flex flex-col justify-between gap-2 my-4">
<div className="flex justify-between">
<h4 className="font-medium h4">{title}</h4>
<p className="h4 font-medium">{year}</p>
<p className="font-medium h4">{year}</p>
</div>
<div className="flex gap-2">
{tags.map(tag => (
@@ -156,7 +156,7 @@ Project.displayName = 'Project';
// }, [sliderOffset, order, slide]);
// return (
// <div className="flex flex-col lg:mt-4 sm:mt-3 mt-2 lg:-mx-10 sm:-mx-6 -mx-4 relative">
// <div className="relative flex flex-col mt-2 -mx-4 lg:mt-4 sm:mt-3 lg:-mx-10 sm:-mx-6">
// <div {...handlers}>
// <div
// ref={ref}
@@ -180,7 +180,7 @@ Project.displayName = 'Project';
// dispatch(-1);
// }
// }}
// className="max-sm:hidden outline-none"
// className="outline-none max-sm:hidden"
// >
// <ArrowLeftIcon />
// </button>
@@ -204,7 +204,7 @@ Project.displayName = 'Project';
// dispatch(1);
// }
// }}
// className="max-sm:hidden outline-none"
// className="outline-none max-sm:hidden"
// >
// <ArrowRightIcon />
// </button>
+4 -1
View File
@@ -8,7 +8,10 @@ export function ProjectsSlider() {
const width = useWindowWidth();
return (
<div className="lg:space-y-14 space-y-6 lg:pt-[100px] sm:py-[70px] py-14">
<div
id="projects"
className="lg:space-y-14 space-y-6 lg:pt-[100px] sm:py-[70px] py-14"
>
<Title>
<span className="text-gradient">Большой опыт в работе</span> с
промышленными предприятиями и учебными заведениями
+10 -10
View File
@@ -5,43 +5,43 @@ import { ArrowDownIcon } from '../icons/ArrowDownIcon';
export function RecentProjects() {
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div className="flex items-center justify-between">
<MiniTitle text="последние проекты" />
<HashLink
className="flex items-center gap-x-2 p-2 bg-[#3D425C] rounded-full"
to={'#projects'}
>
<span className="btn-text font-medium ml-3">Смотреть все</span>
<div className="bg-white rounded-full p-1">
<span className="ml-3 font-medium btn-text">Смотреть все</span>
<div className="p-1 bg-white rounded-full">
<ArrowDownIcon className="w-5 h-5 text-black" />
</div>
</HashLink>
</div>
<div className="flex justify-stretch gap-x-4 min-w-[calc(752/1600*100%)]">
<div className="flex-1 flex flex-col gap-y-6">
<div className="flex flex-col flex-1 gap-y-6">
<img src="src/assets/recent_projects/plane.png" className="" alt="" />
<div className="space-y-2">
<p className="h3 font-medium">L 410 NG Aircraft</p>
<p className="font-medium h3">L 410 NG Aircraft</p>
<div className="flex items-center gap-x-6">
<p className="flex items-center gap-x-2 h4 font-medium py-2">
<p className="flex items-center py-2 font-medium gap-x-2 h4">
<div className="w-3 h-3 bg-white" />
Презентация
</p>
<p className="flex items-center gap-x-2 h4 font-medium py-2">
<p className="flex items-center py-2 font-medium gap-x-2 h4">
<div className="w-3 h-3 bg-white" />
3D-макет
</p>
</div>
</div>
</div>
<div className="flex-1 flex flex-col gap-y-6">
<div className="flex flex-col flex-1 gap-y-6">
<img src="src/assets/recent_projects/laba.png" className="" alt="" />
<div className="space-y-2">
<p className="h3 font-medium">
<p className="font-medium h3">
Учебная лаборатория определения жирности молока
</p>
<div className="flex items-center gap-x-6">
<p className="flex items-center gap-x-2 h4 font-medium py-2">
<p className="flex items-center py-2 font-medium gap-x-2 h4">
<div className="w-3 h-3 bg-white" />
VR-приложение
</p>
+75
View File
@@ -0,0 +1,75 @@
import countries from 'countries-phone-masks';
import {
CountryCode,
getCountries,
getCountryCallingCode,
} from 'libphonenumber-js';
import { useRef, useState } from 'react';
import { useOnClickOutside } from 'usehooks-ts';
import { ChevronUpIcon } from '../icons/ChevronUpIcon';
import { ChevronDownIcon } from '../icons/ChevronDownIcon';
import { ClassNameWrapper } from '../../hocs/ClassNameWrapper';
export function SelectPhoneCode({
currentPhoneCodeAndCountry: [currentPhoneCode, currentCountry],
onClick,
}: {
currentPhoneCodeAndCountry: [string, CountryCode];
onClick: ([phoneCode, country]: [string, CountryCode]) => void;
}) {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useOnClickOutside(ref, () => setOpen(false));
return (
<div ref={ref} className="relative flex flex-col sm:w-1/3 max-w-[350px]">
<button
className="relative flex items-center justify-between gap-x-1"
onClick={e => {
e.preventDefault();
setOpen(prev => !prev);
}}
>
<img
src={countries.find(c => c.iso === currentCountry)?.flag}
className="w-4 sm:w-6"
alt=""
/>
<p className="h4">{currentPhoneCode}</p>
<ClassNameWrapper
className="flex-1 max-sm:w-4 sm:max-lg:w-5"
element={open ? <ChevronUpIcon /> : <ChevronDownIcon />}
/>
</button>
{open && (
<div className="space-y-1 absolute z-10 bg-[#14161F] top-[100%] -left-1 border border-t-0 rounded-b-lg border-[#3D425C] max-h-[300px] overflow-y-auto overflow-x-hidden">
{getCountries()
.map(country => [`+${getCountryCallingCode(country)}`, country])
.filter(
([phonecode, country]) =>
phonecode !== currentPhoneCode || country !== currentCountry,
)
.map(([phoneCode, country]) => (
<div
key={country}
className="flex items-center gap-x-1 hover:bg-[#3D425C] px-1"
onClick={() => {
onClick([phoneCode, country as CountryCode]);
setOpen(false);
}}
>
<img
src={countries.find(c => c.iso === country)?.flag}
alt=""
className="w-4 sm:w-6"
/>
<p className="flex-1 py-1 cursor-pointer h4">{phoneCode}</p>
</div>
))}
</div>
)}
</div>
);
}
+1 -1
View File
@@ -53,7 +53,7 @@ export function SliderControls({
strokeDashoffset={`-${height / 2}`}
/>
</svg>
<p className="h4 font-medium absolute self-center">
<p className="absolute self-center font-medium h4">
{Math.round(slide) + 1} из {slidesCount}
</p>
</div>
+3 -3
View File
@@ -22,7 +22,7 @@ export function Teaching() {
function TeachingFeaturesForDesktop() {
return (
<div className="max-lg:hidden space-y-4 col-span-7">
<div className="col-span-7 space-y-4 max-lg:hidden">
<div className="p-10 relative aspect-[752/400] border border-[#3D425C] bg-[url(src/assets/teaching/highlight_desktop.png)] bg-[length:0%] hover:bg-[length:100%] bg-no-repeat bg-right-top transition-all overflow-hidden flex justify-between">
<div className="space-y-2 max-w-[calc(380/1600*100vw)]">
<TeachingFeatureTitle>Управление процессом</TeachingFeatureTitle>
@@ -54,7 +54,7 @@ function TeachingFeaturesForDesktop() {
</div>
<img
src="src/assets/teaching/modal.png"
className="rounded-lg -mr-10"
className="-mr-10 rounded-lg"
alt="Управление пользователями"
/>
</div>
@@ -105,7 +105,7 @@ function TeachingFeaturesForDesktop() {
function TeachingFeaturesForOtherScreens() {
return (
<div className="lg:hidden sm:-mx-6 -mx-4 flex flex-col">
<div className="flex flex-col -mx-4 lg:hidden sm:-mx-6">
<div className="sm:flex justify-between max-sm:relative sm:aspect-[768/240] aspect-[6/5] order-1 border-t border-[#3D425C] sm:bg-[url(src/assets/teaching/highlight_tablet.png)] bg-[url(src/assets/teaching/highlight_mobile.png)] bg-[length:0%] hover:bg-[length:100%] bg-no-repeat bg-bottom transition-all sm:pt-6 sm:px-6 pt-5 px-5 overflow-hidden">
<div className="space-y-1">
<TeachingFeatureTitle>
+6 -6
View File
@@ -13,7 +13,7 @@ export function Trainings() {
<span className="text-gradient">варианты комплектации тренажеров</span>,
основываясь на специфике вашего тренировочного процесса
</Title>
<div className="sm:grid lg:grid-cols-12 grid-cols-8 gap-x-4">
<div className="grid-cols-8 sm:grid lg:grid-cols-12 gap-x-4">
<TrainingsFeature
order={1}
src="src/assets/trainings/vr.png"
@@ -58,25 +58,25 @@ function TrainingsFeature({
ref={ref}
className="lg:first:h-[200px] lg:last:h-[200px] lg:h-[176px] sm:flex max-sm:space-y-[42px] items-stretch justify-between sm:py-10 max-sm:pt-5"
>
<div className="sm:space-y-4 lg:w-1/3 sm:w-1/2 col-span-1">
<div className="col-span-1 sm:space-y-4 lg:w-1/3 sm:w-1/2">
<h3 className="font-medium max-sm:mb-2 h3">{title}</h3>
<p className="opacity-60 l-text">{text}</p>
</div>
<div className="flex sm:hidden justify-between items-end">
<div className="flex items-end justify-between sm:hidden">
<p className="text-[#52587A] m-text mb-5">[0{order}]</p>
<div className="flex flex-col items-center">
<img src={src} alt={title} className="relative z-30 w-[50vw]" />
<VrBacklightIcon className="absolute w-[36vw] h-fit" />
</div>
</div>
<div className="md:flex hidden">
<div className="hidden md:flex">
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: +hovered, scale: 1 }}
transition={{
duration: 0.4,
}}
className="-my-10 hidden lg:flex items-center justify-center"
className="items-center justify-center hidden -my-10 lg:flex"
>
<img
src={src}
@@ -85,7 +85,7 @@ function TrainingsFeature({
/>
<VrBacklightIcon className="absolute w-[24vw]" />
</motion.div>
<div className="lg:hidden flex items-center justify-center">
<div className="flex items-center justify-center lg:hidden">
<img
src={src}
className="w-[27vw] relative z-20 h-[calc(27vw*0.6)]"
+5 -5
View File
@@ -24,18 +24,18 @@ export function Video() {
</button>
</div>
{open && (
<div className="fixed top-0 left-0 z-50 w-full h-full flex justify-center items-center overflow-hidden">
<div className="fixed top-0 left-0 z-50 flex items-center justify-center w-full h-full overflow-hidden">
<div className="cursor-default" onClick={e => e.stopPropagation()}>
<div className="absolute top-0 left-0 w-screen h-screen overflow-hidden flex justify-center items-start">
<div className="aspect-video w-full">
<div className="absolute top-0 left-0 flex items-start justify-center w-screen h-screen overflow-hidden">
<div className="w-full aspect-video">
<button
className="absolute top-6 right-6 p-6 z-60 rounded-full border border-white outline-none"
className="absolute p-6 border border-white rounded-full outline-none top-6 right-6 z-60"
onClick={() => setOpen(false)}
>
<CloseIcon />
</button>
<iframe
className="h-full w-full"
className="w-full h-full"
src="https://www.youtube.com/embed/aAGcjf-B42g?si=36dEzF9t4efmUJOA"
title="YouTube video player"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
+1 -8
View File
@@ -1,10 +1,4 @@
export function DangerIcon({
className = '',
animated,
}: {
className?: string;
animated: boolean;
}) {
export function DangerIcon({ animated }: { animated: boolean }) {
return (
<svg
width="45"
@@ -13,7 +7,6 @@ export function DangerIcon({
fill="none"
xmlns="http://www.w3.org/2000/svg"
id="svg3710"
className={className}
color="currentColor"
preserveAspectRatio="xMidYMid"
style={{ maxWidth: '100%', maxHeight: '100%' }}
+1 -8
View File
@@ -1,10 +1,4 @@
export function LaboratoriesIcon({
className = '',
animated,
}: {
className?: string;
animated: boolean;
}) {
export function LaboratoriesIcon({ animated }: { animated: boolean }) {
return (
<svg
id="Ремонти обслуживание"
@@ -13,7 +7,6 @@ export function LaboratoriesIcon({
viewBox="0 0 44 44"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
color="currentColor"
style={{ maxWidth: '100%', maxHeight: '100%' }}
>
+1 -8
View File
@@ -1,10 +1,4 @@
export function PlansIcon({
className = '',
animated,
}: {
className?: string;
animated: boolean;
}) {
export function PlansIcon({ animated }: { animated: boolean }) {
return (
<svg
width="44"
@@ -14,7 +8,6 @@ export function PlansIcon({
xmlns="http://www.w3.org/2000/svg"
id="svg3092"
preserveAspectRatio="xMidYMid"
className={className}
color="currentColor"
>
<defs id="defs393"></defs>
+1 -8
View File
@@ -1,10 +1,4 @@
export function ProcessesIcon({
className = '',
animated,
}: {
className?: string;
animated: boolean;
}) {
export function ProcessesIcon({ animated }: { animated: boolean }) {
return (
<svg
id="Ремонти обслуживание"
@@ -14,7 +8,6 @@ export function ProcessesIcon({
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={{ maxWidth: '100%', maxHeight: '100%' }}
className={className}
>
<path
d="M7 7L38 7L38 38"
+1 -8
View File
@@ -1,10 +1,4 @@
export function SafetyIcon({
className = '',
animated,
}: {
className?: string;
animated: boolean;
}) {
export function SafetyIcon({ animated }: { animated: boolean }) {
return (
<svg
width="44"
@@ -14,7 +8,6 @@ export function SafetyIcon({
xmlns="http://www.w3.org/2000/svg"
id="svg36"
preserveAspectRatio="xMidYMid"
className={className}
color="currentColor"
style={{ maxWidth: '100%', maxHeight: '100%' }}
>
+1 -8
View File
@@ -1,10 +1,4 @@
export function ServiceIcon({
className = '',
animated,
}: {
className?: string;
animated: boolean;
}) {
export function ServiceIcon({ animated }: { animated: boolean }) {
return (
<svg
width="44"
@@ -13,7 +7,6 @@ export function ServiceIcon({
fill="none"
xmlns="http://www.w3.org/2000/svg"
id="svg5549"
className={className}
color="currentColor"
preserveAspectRatio="xMidYMid"
style={{ maxHeight: '100%', maxWidth: '100%' }}
+1 -8
View File
@@ -1,10 +1,4 @@
export function TeachSystemsIcon({
className = '',
animated,
}: {
className?: string;
animated: boolean;
}) {
export function TeachSystemsIcon({ animated }: { animated: boolean }) {
return (
<svg
id="Ремонти обслуживание"
@@ -14,7 +8,6 @@ export function TeachSystemsIcon({
fill="none"
xmlns="http://www.w3.org/2000/svg"
color="currentColor"
className={className}
style={{
maxWidth: '100%',
maxHeight: '100%',
+1 -8
View File
@@ -1,10 +1,4 @@
export function TeamworkIcon({
className = '',
animated,
}: {
className?: string;
animated: boolean;
}) {
export function TeamworkIcon({ animated }: { animated: boolean }) {
return (
<svg
id="Ремонти обслуживание"
@@ -13,7 +7,6 @@ export function TeamworkIcon({
viewBox="0 0 44 44"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
style={{
maxWidth: '100%',
maxHeight: '100%',
+20
View File
@@ -0,0 +1,20 @@
import { ReactNode, useEffect, useRef } from 'react';
interface Props {
element: ReactNode;
className?: string;
}
export function ClassNameWrapper({ element, className }: Props) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!className?.split(' ').length) return;
ref.current?.children
.item(0)
?.classList.add(...className.split(' ').filter(Boolean));
}, [className, element]);
return <div ref={ref}>{element}</div>;
}
+4 -6
View File
@@ -1,5 +1,5 @@
import { PropsWithChildren } from 'react';
import { HashLink } from 'react-router-hash-link';
import { Link } from 'react-router-dom';
export function AnchorLink({
children,
@@ -10,16 +10,14 @@ export function AnchorLink({
className?: string;
}>) {
return (
<div>
<HashLink
<Link
className={
'btn-text font-semibold border-l border-[#3D425C] hidden py-[30px] px-10 min-[1350px]:block hover:bg-[#3D425C] active:bg-[#14161F] outline-none ' +
'btn-text font-semibold border-l last:border-r border-[#3D425C] hidden py-[30px] px-10 xl:block hover:bg-[#3D425C] active:bg-[#14161F] outline-none ' +
className
}
to={route}
>
{children}
</HashLink>
</div>
</Link>
);
}
+1 -1
View File
@@ -34,7 +34,7 @@ export function Button({
icon ? 'pr-4' : ''
} flex gap-1 items-center overflow-hidden w-${width} ${className} justify-between`}
>
<span className="group-hover:opacity-10 opacity-0 bg-black transition-opacity absolute top-0 left-0 w-full h-full"></span>
<span className="absolute top-0 left-0 w-full h-full transition-opacity bg-black opacity-0 group-hover:opacity-10"></span>
<span className={'relative font-medium' + (icon ? '' : ' m-auto')}>
{children}
</span>
+1 -1
View File
@@ -100,7 +100,7 @@ export function SliderWithScaling<T extends { title: string }>({
return (
<div className={'flex flex-col relative ' + className}>
<div className="overflow-hidden lg:-mx-10 sm:-mx-6 -mx-4 h-full">
<div className="h-full -mx-4 overflow-hidden lg:-mx-10 sm:-mx-6">
<div {...handlers} className="h-full">
<div
className={`flex items-${alignItems} gap-x-4 -mr-6 select-none`}
+33 -8
View File
@@ -7,6 +7,7 @@ import { SafetyIcon } from '../components/icons/SafetyIcon';
import { TeamworkIcon } from '../components/icons/TeamworkIcon';
import { LaboratoriesIcon } from '../components/icons/LaboratoriesIcon';
import { TeachSystemsIcon } from '../components/icons/TeachSystemsIcon';
import { ClassNameWrapper } from '../hocs/ClassNameWrapper';
export function getIcon(
type:
@@ -22,13 +23,37 @@ export function getIcon(
className?: string,
) {
return new Map<typeof type, ReactNode>([
['danger', DangerIcon({ className, animated })],
['processes', ProcessesIcon({ className, animated })],
['plans', PlansIcon({ className, animated })],
['service', ServiceIcon({ className, animated })],
['safety', SafetyIcon({ className, animated })],
['teamwork', TeamworkIcon({ className, animated })],
['labs', LaboratoriesIcon({ className, animated })],
['teaching', TeachSystemsIcon({ className, animated })],
[
'danger',
ClassNameWrapper({ element: DangerIcon({ animated }), className }),
],
[
'processes',
ClassNameWrapper({ element: ProcessesIcon({ animated }), className }),
],
[
'plans',
ClassNameWrapper({ element: PlansIcon({ animated }), className }),
],
[
'service',
ClassNameWrapper({ element: ServiceIcon({ animated }), className }),
],
[
'safety',
ClassNameWrapper({ element: SafetyIcon({ animated }), className }),
],
[
'teamwork',
ClassNameWrapper({ element: TeamworkIcon({ animated }), className }),
],
[
'labs',
ClassNameWrapper({ element: LaboratoriesIcon({ animated }), className }),
],
[
'teaching',
ClassNameWrapper({ element: TeachSystemsIcon({ animated }), className }),
],
]).get(type);
}
+1 -1
View File
@@ -7,7 +7,7 @@ export default {
'2xl': '1600px',
},
animation: {
'infinite-scroll': 'infinite-scroll 25s linear infinite',
'infinite-scroll': 'infinite-scroll 30s linear infinite',
},
keyframes: {
'infinite-scroll': {
+35 -14
View File
@@ -923,7 +923,7 @@ chokidar@^3.5.3:
optionalDependencies:
fsevents "~2.3.2"
classnames@^2.2.3:
classnames@^2.2.3, classnames@^2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b"
integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
@@ -967,6 +967,16 @@ convert-source-map@^2.0.0:
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
countries-phone-masks@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/countries-phone-masks/-/countries-phone-masks-1.1.0.tgz#854ae21bf495a2bc3a467c47317220ea87ceac50"
integrity sha512-ns5+L+rvkfg6qfBUawAIU9WDCpbiwcPw6n7c87B4zppaxfIBFMyD5Fte6UDAdwynDtdxa2xV5/4IL/2+Ju9nVg==
country-flag-icons@^1.5.11:
version "1.5.13"
resolved "https://registry.yarnpkg.com/country-flag-icons/-/country-flag-icons-1.5.13.tgz#963596b7fca6602b4b389a4e7b711ef3f33cc0b1"
integrity sha512-4JwHNqaKZ19doQoNcBjsoYA+I7NqCH/mC/6f5cBWvdKzcK5TMmzLpq3Z/syVHMHJuDGFwJ+rPpGizvrqJybJow==
cross-spawn@^7.0.0, cross-spawn@^7.0.2:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@@ -1410,6 +1420,13 @@ inherits@2:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
input-format@^0.3.10:
version "0.3.10"
resolved "https://registry.yarnpkg.com/input-format/-/input-format-0.3.10.tgz#e8a8855e2e89e3b1cd995333f6277c14865f0e35"
integrity sha512-5cFv/kOZD7Ch0viprVkuYPDkAU7HBZYBx8QrIpQ6yXUWbAQ0+RQ8IIojDJOf/RO6FDJLL099HDSK2KoVZ2zevg==
dependencies:
prop-types "^15.8.1"
invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
@@ -1534,6 +1551,11 @@ levn@^0.4.1:
prelude-ls "^1.2.1"
type-check "~0.4.0"
libphonenumber-js@^1.11.5, libphonenumber-js@^1.11.7:
version "1.11.7"
resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.11.7.tgz#efe4fcf816e1982925e9c800d0013b0ee99b8283"
integrity sha512-x2xON4/Qg2bRIS11KIN9yCNYUjhtiEjNyptjX0mX+pyKHecxuJVLIpfX1lq9ZD6CrC/rB+y4GBi18c6CEcUR+A==
lilconfig@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52"
@@ -1827,7 +1849,7 @@ prettier@^3.3.2:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.2.tgz#03ff86dc7c835f2d2559ee76876a3914cec4a90a"
integrity sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==
prop-types@^15.7.2:
prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -1867,6 +1889,17 @@ react-is@^16.13.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-phone-number-input@^3.4.5:
version "3.4.5"
resolved "https://registry.yarnpkg.com/react-phone-number-input/-/react-phone-number-input-3.4.5.tgz#9ceeccca59283eda614f516ac6040524e88e4e01"
integrity sha512-IlLTG0F/2P+72drqGNiYaguV3KOD4EVxQWGJ7YSofbOb6vyCWWLJqQIQsFFNpfrMrXzYtB3G+aHL9tprGfisFw==
dependencies:
classnames "^2.5.1"
country-flag-icons "^1.5.11"
input-format "^0.3.10"
libphonenumber-js "^1.11.5"
prop-types "^15.8.1"
react-rangeslider@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/react-rangeslider/-/react-rangeslider-2.2.0.tgz#4362b01f4f5a455f0815d371d496f69ca4c6b5aa"
@@ -1888,13 +1921,6 @@ react-router-dom@^6.23.1:
"@remix-run/router" "1.16.1"
react-router "6.23.1"
react-router-hash-link@^2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/react-router-hash-link/-/react-router-hash-link-2.4.3.tgz#570824d53d6c35ce94d73a46c8e98673a127bf08"
integrity sha512-NU7GWc265m92xh/aYD79Vr1W+zAIXDWp3L2YZOYP4rCqPnJ6LI6vh3+rKgkidtYijozHclaEQTAHaAaMWPVI4A==
dependencies:
prop-types "^15.7.2"
react-router@6.23.1:
version "6.23.1"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.23.1.tgz#d08cbdbd9d6aedc13eea6e94bc6d9b29cb1c4be9"
@@ -1907,11 +1933,6 @@ 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"