fixes, todo contacts form and animations

This commit is contained in:
2024-07-10 14:54:16 +05:00
parent c737808a71
commit 9186673043
23 changed files with 851 additions and 380 deletions
+1
View File
@@ -0,0 +1 @@
PUBLIC_API_URL=https://graff.estate/api
+5
View File
@@ -11,16 +11,21 @@
},
"dependencies": {
"framer-motion": "^11.2.14",
"ky": "^1.4.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-input-mask": "^2.0.4",
"react-router-dom": "^6.23.1",
"react-router-hash-link": "^2.4.3",
"react-swipeable": "^7.0.1",
"usehooks-ts": "^3.1.0",
"zustand": "^4.5.4"
},
"devDependencies": {
"@types/node": "^20.14.10",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react-input-mask": "^3.0.5",
"@types/react-router-hash-link": "^2.4.9",
"@typescript-eslint/eslint-plugin": "^7.13.1",
"@typescript-eslint/parser": "^7.13.1",
+7
View File
@@ -0,0 +1,7 @@
import ky from 'ky';
const api = ky.extend({
prefixUrl: process.env.PUBLIC_API_URL,
});
export default api;
+18 -2
View File
@@ -1,13 +1,21 @@
import { PropsWithChildren, useState } from 'react';
import { PropsWithChildren, useRef, useState } from 'react';
import { NavLink } from '../../ui/NavLink';
import { Link } from 'react-router-dom';
import { Lang, useLang } from '../../store/language';
import { HashLink } from 'react-router-hash-link';
import { useOnClickOutside } from 'usehooks-ts';
export function Navbar() {
const [menuOpen, setMenuOpen] = useState(false);
const { value: lang } = useLang();
const menuRef = useRef<HTMLDivElement>(null);
const menuBtnRef = useRef<HTMLButtonElement>(null);
useOnClickOutside<HTMLDivElement | HTMLButtonElement>(
[menuRef, menuBtnRef],
() => setMenuOpen(false),
);
return (
<>
<nav className="flex items-stretch justify-between border-b border-[#3D425C] desktop:pl-10 mobile:pl-4 desktop:min-h-[72px] mobile:min-h-16">
@@ -29,6 +37,7 @@ export function Navbar() {
</button>
<LangToggler lang={lang} />
<button
ref={menuBtnRef}
onClick={() => setMenuOpen(prev => !prev)}
className="px-6 py-5 min-[1350px]:hidden mobile:block border-[#3D425C] mobile:max-tablet:border-l"
>
@@ -41,6 +50,7 @@ export function Navbar() {
</nav>
{menuOpen && (
<div
ref={menuRef}
onClick={() => setMenuOpen(false)}
className={
'absolute z-50 w-full min-[1350px]:hidden tablet:max-[1350px]:max-w-[340px] right-0 tablet:border-l border-b border-[#3D425C]' +
@@ -108,8 +118,14 @@ function ChooseLang({
function LangToggler({ lang }: { lang: Lang }) {
const [open, setOpen] = useState(false);
const langTogglerRef = useRef<HTMLDivElement>(null);
useOnClickOutside(langTogglerRef, () => setOpen(false));
return (
<div className="min-w-[101px] mobile:max-[1349px]:hidden box-border border-r border-[#3D425C]">
<div
ref={langTogglerRef}
className="min-w-[101px] mobile:max-[1349px]:hidden box-border border-r border-[#3D425C]"
>
<button
onClick={() => setOpen(prev => !prev)}
className="mx-6 h-full gap-x-1 items-center flex text-[#ffffff] font-semibold box-border btn-text"
+16 -28
View File
@@ -1,20 +1,7 @@
import AppearanceText from '../../ui/AppearanceText';
import { AppearanceText } from '../../ui/AppearanceText';
import { MiniTitle } from '../../ui/MiniTitle';
import { Title } from '../../ui/Title';
const splits = [
'В одном ',
'цифровом ',
'пространстве ',
'могут работать ',
'сотрудники, ',
'находящиеся ',
'в разных ',
'помещениях, ',
'зданиях ',
'или городах',
];
export function Availables() {
return (
<div className="desktop:py-[70px] desktop:px-10 mobile:py-14 tablet:px-6 mobile:px-4">
@@ -49,20 +36,21 @@ export function Availables() {
text="координация действий между несколькими сотрудниками"
/>
</div>
<h3
className={
'text-[#ffffff] font-medium tablet-figma:max-w-[668px] desktop-figma:max-w-[39.25vw] h3 '
}
>
{splits.map((text, index) => (
<AppearanceText
text={text}
key={text}
opacity={index > 2 ? 60 : 100}
duration={3 + index}
/>
))}
</h3>
<AppearanceText
className="tablet-figma:max-w-[668px] desktop-figma:max-w-[39.25vw]"
splits={[
'В одном ',
'цифровом ',
'пространстве ',
'могут работать ',
'сотрудники, ',
'находящиеся ',
'в разных ',
'помещениях, ',
'зданиях ',
'или городах',
]}
/>
</div>
</div>
</div>
+203
View File
@@ -0,0 +1,203 @@
import { ChangeEvent, FormEvent, useState } from 'react';
import api from '../../api/contactsFormInstance';
import { Close2Icon } from '../icons/Close2Icon';
import { AsteriskIcon } from '../icons/AstreskIcon';
import InputMask from 'react-input-mask';
import Button from '../../ui/Button';
import LoaderIcon from '../icons/LoaderIcon';
import SendIcon from '../icons/SendIcon';
import CheckGradientIcon from '../icons/CheckGradientIcon';
function ContactsForm() {
const [name, setName] = useState<string>('');
const [phone, setPhone] = useState<string>('');
const [email, setEmail] = useState<string>('');
const [description, setDescription] = useState<string>('');
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isSend, setIsSend] = useState<boolean>(false);
function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
sendMail();
}
async function sendMail() {
setIsLoading(true);
try {
await api
.post('mail', {
json: {
fullname: name,
phone,
email,
request: description,
},
})
.json();
setIsSend(true);
setIsLoading(false);
} catch (error) {
setIsLoading(false);
if (error instanceof Error) {
alert(error.message);
}
}
}
return (
<>
{!isSend ? (
<div className="flex flex-col gap-5">
<div className="flex justify-between items-center">
<p className="font-gilroy text-gradient sm:text-2xl text-xl w-fit font-semibold">
Свяжитесь с нами
</p>
<button className="p-2 hover:bg-white hover:bg-opacity-10 transition-colors rounded-full">
<Close2Icon />
</button>
</div>
<form onSubmit={handleSubmit} className="flex flex-col gap-6">
<div>
<div className="relative">
<input
required
type="text"
value={name}
onChange={e => setName(e.target.value)}
className="feedback-field bg-transparent border border-[#3D425C] rounded-none sm:pt-12 sm:pb-4 sm:px-4 pt-8 pb-3 px-3 outline-none outline-1 -outline-offset-1 focus:outline-[#D375FF] transition-all w-full"
/>
<p className="feedback-placeholder lg:text-base text-sm absolute sm:pt-4 sm:pb-4 sm:px-4 sm:top-4 pt-3 pb-3 px-3 top-3 w-full opacity-50 transition-all pointer-events-none flex justify-between items-center">
<span>Имя</span>
<AsteriskIcon />
</p>
</div>
<div className="relative">
<InputMask
required
type="tel"
mask={'+999999999999999'}
maskChar={null}
value={phone}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setPhone(e.target.value)
}
className={[
'feedback-field bg-transparent border rounded-none border-t-0 border-[#3D425C] sm:pt-12 sm:pb-4 sm:px-4 pt-8 pb-3 px-3 outline-none outline-1 -outline-offset-1 focus:outline-[#D375FF] transition-all w-full',
].join(' ')}
/>
<p className="feedback-placeholder lg:text-base text-sm absolute sm:pt-4 sm:pb-4 sm:px-4 sm:top-4 pt-3 pb-3 px-3 top-3 w-full opacity-50 transition-all pointer-events-none flex justify-between items-center">
<span>Телефон</span>
<AsteriskIcon />
</p>
</div>
<div className="relative">
<input
required
type="text"
value={email}
onChange={e => setEmail(e.target.value)}
className="feedback-field bg-transparent border rounded-none border-t-0 border-[#3D425C] sm:pt-12 sm:pb-4 sm:px-4 pt-8 pb-3 px-3 outline-none outline-1 -outline-offset-1 focus:outline-[#D375FF] transition-all w-full"
/>
<p className="feedback-placeholder lg:text-base text-sm absolute sm:pt-4 sm:pb-4 sm:px-4 sm:top-4 pt-3 pb-3 px-3 top-3 w-full opacity-50 transition-all pointer-events-none flex justify-between items-center">
<span>Email</span>
<AsteriskIcon />
</p>
</div>
<div className="relative">
<textarea
placeholder="Опишите вашу задачу"
value={description}
onChange={e => setDescription(e.target.value)}
className="feedback-field bg-transparent resize-none border rounded-none border-t-0 border-[#3D425C] p-4 sm:min-h-[192px] min-h-[128px] outline-none outline-1 -outline-offset-1 focus:outline-[#D375FF] transition-all w-full"
></textarea>
</div>
<div
className="border border-t-0 border-[#3D425C] 2xl:p-6 p-4 sm:mt-0 flex items-center"
style={{ marginTop: '-6px' }}
>
<div className="text-xs leading-tight">
Нажимая кнопку отправить, вы принимаете{' '}
<a
href="https://graff.tech/privacypolicy"
target="_blank"
className="text-[#798FFF] cursor-pointer opacity-95 hover:opacity-100 transition-all"
>
условия использования
</a>{' '}
и{' '}
<a
href="https://graff.tech/privacypolicy"
target="_blank"
className="text-[#798FFF] cursor-pointer opacity-95 hover:opacity-100 transition-all"
>
политику конфиденциальности
</a>
</div>
</div>
<div className="border border-t-0 border-[#3D425C] 2xl:p-6 p-4 sm:mt-0 text-xs flex items-center gap-2">
<div className="flex gap-2">
<div className="">
<AsteriskIcon />
</div>
<p></p>
<p>
Звездочкой отмечены обязательные
<br />
для заполнения поля
</p>
</div>
</div>
</div>
<Button
width="full"
disabled={isLoading}
icon={
isLoading ? (
<LoaderIcon className="relative 2xl:w-8 2xl:h-8 w-6 h-6 animate-spin" />
) : (
<SendIcon className="relative 2xl:w-8 2xl:h-8 w-6 h-6" />
)
}
className="py-4"
>
Отправить
</Button>
</form>
</div>
) : (
<div className="flex flex-col gap-4">
<div className="flex justify-between items-center">
<p className="font-gilroy text-gradient sm:text-2xl text-xl w-fit font-semibold flex items-center gap-2">
<span>Заявка отправлена</span>
<CheckGradientIcon className="lg:w-8 lg:h-8 w-6 h-6" />
</p>
<button className="p-2 hover:bg-white hover:bg-opacity-10 transition-colors rounded-full">
<Close2Icon />
</button>
</div>
<div className="flex flex-col gap-2">
<p className="font-gilroy leading-snug lg:text-2xl text-xl font-semibold">
Спасибо за подачу заявки!
</p>
<p className="lg:text-base text-sm">
Мы ценим ваш интерес к нашей компании и в ближайшее время свяжемся
с вами для уточнения деталей проекта.
</p>
</div>
</div>
)}
</>
);
}
export default ContactsForm;
+18 -30
View File
@@ -1,20 +1,5 @@
import { MiniTitle } from '../../ui/MiniTitle';
import AppearanceText from '../../ui/AppearanceText';
const splits = [
'В тренажере человек ',
'принимает решения ',
'так же, ',
'как в реальном мире, ',
'активируя ',
'те же нейронные ',
'цепочки в мозгу. ',
'Это позволяет ',
'добиться ',
'реальной ',
'производительности ',
'в работе. ',
];
import { AppearanceText } from '../../ui/AppearanceText';
export function Effeciency() {
return (
@@ -39,20 +24,23 @@ export function Effeciency() {
title={'готовность к опасным ситуациямние выше на'}
/>
</div>
<h3
className={
'text-[#ffffff] font-medium h3 max-w-[752px] desktop-figma:max-w-[47vw] '
}
>
{splits.map((text, index) => (
<AppearanceText
key={text}
text={text}
opacity={index > 3 ? 60 : 100}
duration={3 + index}
/>
))}
</h3>
<AppearanceText
className="max-w-[752px] desktop-figma:max-w-[47vw]"
splits={[
'В тренажере человек ',
'принимает решения ',
'так же, ',
'как в реальном мире, ',
'активируя ',
'те же нейронные ',
'цепочки в мозгу. ',
'Это позволяет ',
'добиться ',
'реальной ',
'производительности ',
'в работе. ',
]}
/>
</div>
</div>
</div>
+3 -3
View File
@@ -11,7 +11,7 @@ export function Events() {
<div className="flex desktop:border-t mobile:max-desktop:flex-col border-[#3D425C] pt-5 gap-x-4 w-full">
<MiniTitle text="события" className="mobile:max-tablet:mb-2" />
<div className="desktop:max-desktop-figma:min-w-[clamp(688px,688px+(100vw-1024px)/576*425,1133px)] desktop-figma:min-w-[70.9vw]">
<div className="py-5 tablet:border-b mobile:max-tablet:border-t border-[#3D425C] flex justify-between mobile:max-tablet:flex-col items-start gap-x-4">
<div className="py-7 tablet:border-b mobile:max-tablet:border-t border-[#3D425C] flex justify-between mobile:max-tablet:flex-col items-start gap-x-4">
<div className="w-fit">
<EventTitle className="tablet:mb-8 w-fit">
Макет кабины машиниста «Иволга» на выставке ВДНХ
@@ -31,11 +31,11 @@ export function Events() {
</div>
<LinkButton href="/" />
</div>
<div className="py-5 tablet:border-b mobile:max-tablet:border-t border-[#3D425C] flex mobile:max-tablet:flex-col tablet:items-center justify-between gap-x-4">
<div className="py-7 tablet:border-b mobile:max-tablet:border-t border-[#3D425C] flex mobile:max-tablet:flex-col tablet:items-center justify-between gap-x-4">
<EventTitle>Победа на BuildUP 2023 в номинации IT</EventTitle>
<LinkButton href="/" />
</div>
<div className="py-5 tablet:border-b mobile:max-tablet:border-t border-[#3D425C] flex mobile:max-tablet:flex-col tablet:items-center justify-between gap-x-4">
<div className="py-7 tablet:border-b mobile:max-tablet:border-t border-[#3D425C] flex mobile:max-tablet:flex-col tablet:items-center justify-between gap-x-4">
<EventTitle>
Транспортное и специальное тренажеростроение 2023
</EventTitle>
@@ -0,0 +1,68 @@
export function ForTeachingTab() {
return (
<div className="desktop:bg-[url('src/assets/mask_group2.png')] bg-[#3D425C4D] bg-no-repeat desktop:p-10 tablet:p-7 mobile:p-5 rounded-xl 2xl:bg-contain bg-[right] desktop:max-2xl:bg-[length:50%]">
<div className='tablet:max-desktop:bg-[url("src/assets/mask_group2.png")] bg-no-repeat bg-right bg-[length:50%] tablet:max-desktop:pb-[55px] mobile:max-desktop:border-b border-[#3D425C] tablet:mb-8 mobile:mb-4'>
<h3 className="text-[#ffffff] font-medium desktop:max-w-[455px] tablet:max-w-[326px] mobile:max-tablet:mb-5 h3">
Интерактивные тренажеры для учебных заведений
</h3>
<img
src="src/assets/mask_group2.png"
className="tablet:hidden"
alt=""
/>
</div>
<div className="flex desktop:flex-col mobile:max-tablet:flex-col desktop:gap-y-6 mobile:gap-y-4 desktop:mb-12 tablet:max-desktop:mb-8 mobile:max-tablet:mb-5 tablet:max-desktop:border-b border-[#3D425C] tablet:max-desktop:pb-8">
<div className="flex gap-x-7 items-start desktop:max-w-[437px] tablet:w-fit tablet:max-desktop:pr-3 mobile:max-tablet:pb-4 mobile:max-tablet:border-b border-[#3D425C]">
<img
src="src/assets/service_icon.svg"
className="mobile:max-desktop:hidden"
alt=""
/>
<div className="desktop:pl-4 tablet:pl-[13px] tablet:border-l border-[#3D425C]">
<div className="flex mobile:max-tablet:items-center tablet:items-start tablet:max-desktop:flex-col gap-x-2 mb-2">
<img
src="src/assets/service_icon.svg"
className="desktop:hidden"
alt=""
/>
<h4 className="text-[#ffffff] font-medium l-text">
cоздание обучающих VR систем
</h4>
</div>
<p className="text-[#ffffff] opacity-60 desktop:font-medium m-text">
Проведение виртуальных практических работ, создание учебных
мастерских и стендов
</p>
</div>
</div>
<div className="flex gap-x-7 items-start desktop:max-w-[437px] tablet:w-fit tablet:max-desktop:pr-3 mobile:max-tablet:pb-4 mobile:max-tablet:border-b border-[#3D425C]">
<img
src="src/assets/service_icon.svg"
className="mobile:max-desktop:hidden"
alt=""
/>
<div className="desktop:pl-4 tablet:pl-[13px] tablet:border-l border-[#3D425C]">
<div className="flex mobile:max-tablet:items-center tablet:items-start tablet:max-desktop:flex-col gap-x-2 mb-2">
<img
src="src/assets/service_icon.svg"
className="desktop:hidden"
alt=""
/>
<h4 className="text-[#ffffff] font-medium l-text">
cоздание VR лабораторий
</h4>
</div>
<p className="text-[#ffffff] opacity-60 desktop:font-medium m-text">
Тренажер для проведения лабораториных работ позволит избежать
поломки оборудования, а также экономить на расходных средствах
</p>
</div>
</div>
</div>
<h4 className="text-[#ffffff] font-medium desktop:max-w-[408px] l-text">
Оснащение учебных классов и центров всем необходимым для современного
обучения под «ключ»
</h4>
</div>
);
}
@@ -0,0 +1,150 @@
import { useEffect, useReducer, useState } from 'react';
import { useWindowWidth } from '../../../hooks/useWindowWidth';
import { useSwipeable } from 'react-swipeable';
export function SimulatorsTab() {
const width = useWindowWidth();
const [slide, setSlide] = useState(0);
const [sliderOffset, setSliderOffset] = useState(-width + 80);
const [order, dispatch] = useReducer(
(state: string[], action: string) => {
switch (action) {
case 'prev':
setSliderOffset(-(width - 80) * 2);
return [state[state.length - 3], ...state.slice(0, -1)];
case 'next':
setSliderOffset(prev => prev + width - 80);
return [...state.slice(1), state[2]];
default:
return state;
}
},
[
'src/assets/rzhd2.png',
'src/assets/train.png',
'src/assets/dispatcher.png',
'src/assets/winda.png',
'src/assets/rzhd.png',
'src/assets/rzhd2.png',
'src/assets/train.png',
],
);
useEffect(() => {
setSliderOffset(-width + 80);
}, [order, slide, width]);
const handlers = useSwipeable({
onSwipedLeft: () => {
dispatch('next');
setSlide(prev => (prev === order.length - 3 ? 0 : prev + 1));
},
onSwipedRight: () => {
dispatch('prev');
setSlide(prev => (prev === 0 ? order.length - 3 : prev - 1));
},
trackMouse: true,
preventScrollOnSwipe: true,
touchEventOptions: { passive: true },
});
return (
<div className="bg-[#3D425C4D] rounded-xl desktop:p-10 tablet:max-desktop:p-7 mobile:max-tablet:p-5 select-none overflow-hidden">
<div className="tablet:flex justify-between gap-x-2">
<div className="desktop:max-w-[539px] tablet:max-w-[50vw]">
<h3 className="max-w-[455px] text-[#ffffff] font-medium h3 desktop:mb-8 tablet:mb-6 mobile:mb-5">
Интерактивные симуляторы управления техникой
</h3>
<ul className="flex flex-wrap gap-2 desktop:mb-12 tablet:max-desktop:mb-10 mobile:mb-5 tablet:max-desktop:max-w-[325px]">
<SimulatorsItem text="авиационные симуляторы" />
<SimulatorsItem text="погрузчики – ричстракеры" />
<SimulatorsItem text="тяговые составы железной дороги" />
<SimulatorsItem text="грузовые краны" />
<SimulatorsItem text="вертолетная техника" />
<SimulatorsItem text="горные самосвалы и экскаваторы" />
</ul>
<div className="max-w-[455px] mobile:max-tablet:mb-5">
<h4 className="text-[#ffffff] font-medium l-text desktop:mb-2 mobile:max-tablet:mb-3">
В основу симуляторов заложена математическая модель, полностью
соответствующая работе настоящего оборудования
</h4>
<p className="tablet:max-desktop:hidden text-[#ffffff] opacity-60 m-text">
модель позволяет производить расчеты характеристик работы,
отслеживать безопасность работы устройств и симулировать
внештатные ситуации.
</p>
</div>
</div>
<div className="self-center tablet:max-desktop:max-w-[234px]">
<div {...handlers}>
<div
className="flex desktop:justify-end select-none mobile:max-tablet:relative xl:max-desktop-figma:max-w-[clamp(553px,553px+(100vw-1280px)/320*160,713px)] desktop-figma:max-w-[calc((100vw-256px)*0.53)] desktop:max-xl:max-w-[300px] tablet:max-desktop:flex-col tablet:flex-wrap gap-2 tablet:max-desktop:mb-10 mobile:max-tablet:duration-1000"
style={
width < 640
? {
transform: `translateX(${sliderOffset}px)`,
transition: `${sliderOffset === 0 || sliderOffset === (-width + 80) * 2 ? 0 : 0.4}s`,
}
: {}
}
{...handlers}
>
{width < 640 ? (
order.map((src, index) => (
<img
key={index}
src={src}
className="rounded-lg mobile:max-tablet:min-w-[clamp(280px,100vw-80px,559px)] object-cover pointer-events-none"
alt=""
/>
))
) : (
<>
<img
src="src/assets/train.png"
className="rounded-lg xl:max-desktop-figma:w-[clamp(178px,178px+(100vw-1280px)/320*54,232px)] desktop-figma:w-[calc((100vw-256px)*0.145)] tablet:max-xl:hidden"
alt=""
/>
<img
src="src/assets/dispatcher.png"
className="xl:max-desktop-figma:w-[clamp(178px,178px+(100vw-1280px)/320*54,232px)] desktop-figma:w-[calc((100vw-256px)*0.145)] tablet:max-xl:hidden"
alt=""
/>
<img
src="src/assets/winda.png"
className="xl:max-desktop-figma:w-[clamp(178px,178px+(100vw-1280px)/320*54,232px)] desktop-figma:w-[calc((100vw-256px)*0.145)] tablet:max-xl:hidden"
alt=""
/>
<img
src="src/assets/rzhd.png"
className="xl:max-desktop-figma:w-[clamp(272px,272px+(100vw-1280px)/320*80,352px)] desktop-figma:w-[calc((100vw-256px)*0.22)]"
alt=""
/>
<img
src="src/assets/rzhd2.png"
className="xl:max-desktop-figma:w-[clamp(272px,272px+(100vw-1280px)/320*80,352px)] desktop-figma:w-[calc((100vw-256px)*0.22)]"
alt=""
/>
</>
)}
</div>
</div>
<p className="desktop:hidden mobile:max-tablet:hidden text-[#ffffff] opacity-60 m-text">
модель позволяет производить расчеты характеристик работы,
отслеживать безопасность работы устройств и симулировать внештатные
ситуации.
</p>
</div>
</div>
</div>
);
}
function SimulatorsItem({ text }: { text: string }) {
return (
<li className="text-[#ffffff] l-text bg-[#3D425C4D] rounded-[44px] desktop:px-5 desktop:py-2 mobile:px-4 mobile:py-[6px]">
{text}
</li>
);
}
@@ -0,0 +1,72 @@
export function TrainingsTab() {
return (
<div className="bg-[#3D425C4D] rounded-xl desktop:bg-[url('src/assets/mask_group.png')] desktop-figma:bg-[length:70%] bg-right-bottom desktop:bg-[length:55%] desktop:p-10 tablet:max-desktop:p-7 mobile:max-tablet:p-5 bg-no-repeat">
<div className="desktop:max-w-[455px]">
<div className="tablet:max-desktop:border-b border-[#3D425C] pb-5 tablet:max-desktop:bg-[url('src/assets/mask_group.png')] bg-no-repeat bg-contain bg-right-bottom tablet:max-tablet-figma:bg-[length:40%]">
<div className="tablet:max-desktop:max-w-[326px] mobile:max-tablet:border-b border-[#3D425C]">
<h1 className="text-[#ffffff] font-medium tablet:mb-8 tablet-figma:text-[clamp(24px,24px+(100vw-768px)/832*8,32px)] tablet-figma:leading-[clamp(24px,24px+(100vw-768px)/832*4.8,28.8px)] mobile:max-tablet-figma:text-[clamp(20px,20px+(100vw-360px)/408*4,24px)] mobile:max-tablet-figma:leading-[clamp(20px,20px+(100vw-360px)/408*4,24px)]">
Промышленные тренажеры виртуальной реальности
</h1>
<p className="desktop:hidden tablet:max-desktop:mt-11 mobile:max-tablet:mt-2 m-text text-[#ffffff]">
Может быть еще какой-нибудь небольшой текст, а то мне не хватает
для балланса. Ну если не будет, то как-нибудь переживем
</p>
<img
src="src/assets/mask_group_big.svg"
className="tablet:hidden mt-4 object-cover w-full"
alt=""
/>
</div>
</div>
<div className="flex desktop:flex-col tablet:max-desktop:mt-8 desktop:gap-y-6 mobile:max-tablet:flex-col mobile:max-tablet:gap-y-4 tablet:max-desktop:gap-x-3">
<TeachingItem
iconType="danger"
text="Отработка проведения технологических операций: оказание первой помощи, работы на высоте, работа с доменной печью и т.п."
title="обучение навыкам работы на опасных производствах"
/>
<TeachingItem
iconType="service"
text="отработка определения неисправностей оборудования и выполнения ремонтных работ на цифровом двойнике"
title="обучение обслуживанию и ремонту оборудования, систем, техники"
/>
<TeachingItem
iconType="safety"
text="отработка плана мероприятий по локализации и ликвидации последствий аварий. "
title="обучение правилам промышленной безопасности и охраны труда"
/>
</div>
</div>
</div>
);
}
function TeachingItem({
title,
text,
iconType,
}: {
title: string;
text: string;
iconType: 'danger' | 'service' | 'safety';
}) {
return (
<div className="desktop:border-l-0 tablet:border-l mobile:max-tablet:first:border-t-0 mobile:max-tablet:border-t border-[#3D425C] desktop:flex desktop:items-start desktop:gap-x-7 tablet:max-desktop:pl-3 mobile:max-tablet:pt-4">
<img
src={`src/assets/${iconType}_icon.svg`}
alt=""
className="mobile:max-tablet:hidden tablet:max-desktop:mb-[14px]"
/>
<div className="desktop:border-l border-[#3D425C] desktop:pl-4">
<h4 className="text-[#ffffff] flex items-center gap-x-2 font-medium l-text mb-2">
<img
src={`src/assets/${iconType}_icon.svg`}
alt=""
className="tablet:hidden"
/>
{title}
</h4>
<p className="text-[#ffffff] m-text opacity-60">{text}</p>
</div>
</div>
);
}
+4 -292
View File
@@ -1,8 +1,9 @@
import { useEffect, useReducer, useRef, useState } from 'react';
import { useRef, useState } from 'react';
import { MiniTitle } from '../../ui/MiniTitle';
import { Title } from '../../ui/Title';
import { useWindowWidth } from '../../hooks/useWindowWidth';
import { useSwipeable } from 'react-swipeable';
import { ForTeachingTab } from './ProductTabs/ForTeachingTab';
import { SimulatorsTab } from './ProductTabs/SimulatorsTab';
import { TrainingsTab } from './ProductTabs/TrainingsTab';
export function Products() {
const [curTab, setCurTab] = useState(0);
@@ -97,292 +98,3 @@ function TabButton({
</button>
);
}
function TeachingItem({
title,
text,
iconType,
}: {
title: string;
text: string;
iconType: 'danger' | 'service' | 'safety';
}) {
return (
<div className="desktop:border-l-0 tablet:border-l mobile:max-tablet:first:border-t-0 mobile:max-tablet:border-t border-[#3D425C] desktop:flex desktop:items-start desktop:gap-x-7 tablet:max-desktop:pl-3 mobile:max-tablet:pt-4">
<img
src={`src/assets/${iconType}_icon.svg`}
alt=""
className="mobile:max-tablet:hidden tablet:max-desktop:mb-[14px]"
/>
<div className="desktop:border-l border-[#3D425C] desktop:pl-4">
<h4 className="text-[#ffffff] flex items-center gap-x-2 font-medium l-text mb-2">
<img
src={`src/assets/${iconType}_icon.svg`}
alt=""
className="tablet:hidden"
/>
{title}
</h4>
<p className="text-[#ffffff] m-text opacity-60">{text}</p>
</div>
</div>
);
}
function TrainingsTab() {
return (
<div className="bg-[#3D425C4D] rounded-xl desktop:bg-[url('src/assets/mask_group.png')] desktop-figma:bg-[length:70%] bg-right-bottom desktop:bg-[length:55%] desktop:p-10 tablet:max-desktop:p-7 mobile:max-tablet:p-5 bg-no-repeat">
<div className="desktop:max-w-[455px]">
<div className="tablet:max-desktop:border-b border-[#3D425C] pb-5 tablet:max-desktop:bg-[url('src/assets/mask_group.png')] bg-no-repeat bg-contain bg-right-bottom tablet:max-tablet-figma:bg-[length:40%]">
<div className="tablet:max-desktop:max-w-[326px] mobile:max-tablet:border-b border-[#3D425C]">
<h1 className="text-[#ffffff] font-medium tablet:mb-8 tablet-figma:text-[clamp(24px,24px+(100vw-768px)/832*8,32px)] tablet-figma:leading-[clamp(24px,24px+(100vw-768px)/832*4.8,28.8px)] mobile:max-tablet-figma:text-[clamp(20px,20px+(100vw-360px)/408*4,24px)] mobile:max-tablet-figma:leading-[clamp(20px,20px+(100vw-360px)/408*4,24px)]">
Промышленные тренажеры виртуальной реальности
</h1>
<p className="desktop:hidden tablet:max-desktop:mt-11 mobile:max-tablet:mt-2 m-text text-[#ffffff]">
Может быть еще какой-нибудь небольшой текст, а то мне не хватает
для балланса. Ну если не будет, то как-нибудь переживем
</p>
<img
src="src/assets/mask_group_big.svg"
className="tablet:hidden mt-4 object-cover w-full"
alt=""
/>
</div>
</div>
<div className="flex desktop:flex-col tablet:max-desktop:mt-8 desktop:gap-y-6 mobile:max-tablet:flex-col mobile:max-tablet:gap-y-4 tablet:max-desktop:gap-x-3">
<TeachingItem
iconType="danger"
text="Отработка проведения технологических операций: оказание первой помощи, работы на высоте, работа с доменной печью и т.п."
title="обучение навыкам работы на опасных производствах"
/>
<TeachingItem
iconType="service"
text="отработка определения неисправностей оборудования и выполнения ремонтных работ на цифровом двойнике"
title="обучение обслуживанию и ремонту оборудования, систем, техники"
/>
<TeachingItem
iconType="safety"
text="отработка плана мероприятий по локализации и ликвидации последствий аварий. "
title="обучение правилам промышленной безопасности и охраны труда"
/>
</div>
</div>
</div>
);
}
function SimulatorsTab() {
const width = useWindowWidth();
const [slide, setSlide] = useState(0);
const [sliderOffset, setSliderOffset] = useState(-width + 80);
const [order, dispatch] = useReducer(
(state: string[], action: string) => {
switch (action) {
case 'prev':
setSliderOffset(-(width - 80) * 2);
return [state[state.length - 3], ...state.slice(0, -1)];
case 'next':
setSliderOffset(prev => prev + width - 80);
return [...state.slice(1), state[2]];
default:
return state;
}
},
[
'src/assets/rzhd2.png',
'src/assets/train.png',
'src/assets/dispatcher.png',
'src/assets/winda.png',
'src/assets/rzhd.png',
'src/assets/rzhd2.png',
'src/assets/train.png',
],
);
useEffect(() => {
setSliderOffset(-width + 80);
}, [order, slide, width]);
const handlers = useSwipeable({
onSwipedLeft: () => {
dispatch('next');
setSlide(prev => (prev === order.length - 3 ? 0 : prev + 1));
},
onSwipedRight: () => {
dispatch('prev');
setSlide(prev => (prev === 0 ? order.length - 3 : prev - 1));
},
trackMouse: true,
preventScrollOnSwipe: true,
touchEventOptions: { passive: true },
});
return (
<div className="bg-[#3D425C4D] rounded-xl desktop:p-10 tablet:max-desktop:p-7 mobile:max-tablet:p-5 select-none overflow-hidden">
<div className="tablet:flex justify-between gap-x-2">
<div className="desktop:max-w-[539px] tablet:max-w-[50vw]">
<h3 className="max-w-[455px] text-[#ffffff] font-medium h3 desktop:mb-8 tablet:mb-6 mobile:mb-5">
Интерактивные симуляторы управления техникой
</h3>
<ul className="flex flex-wrap gap-2 desktop:mb-12 tablet:max-desktop:mb-10 mobile:mb-5 tablet:max-desktop:max-w-[325px]">
<SimulatorsItem text="авиационные симуляторы" />
<SimulatorsItem text="погрузчики – ричстракеры" />
<SimulatorsItem text="тяговые составы железной дороги" />
<SimulatorsItem text="грузовые краны" />
<SimulatorsItem text="вертолетная техника" />
<SimulatorsItem text="горные самосвалы и экскаваторы" />
</ul>
<div className="max-w-[455px] mobile:max-tablet:mb-5">
<h4 className="text-[#ffffff] font-medium l-text desktop:mb-2 mobile:max-tablet:mb-3">
В основу симуляторов заложена математическая модель, полностью
соответствующая работе настоящего оборудования
</h4>
<p className="tablet:max-desktop:hidden text-[#ffffff] opacity-60 m-text">
модель позволяет производить расчеты характеристик работы,
отслеживать безопасность работы устройств и симулировать
внештатные ситуации.
</p>
</div>
</div>
<div className="self-center tablet:max-desktop:max-w-[234px]">
<div {...handlers}>
<div
className="flex desktop:justify-end select-none mobile:max-tablet:relative xl:max-desktop-figma:max-w-[clamp(553px,553px+(100vw-1280px)/320*160,713px)] desktop-figma:max-w-[calc((100vw-256px)*0.53)] desktop:max-xl:max-w-[300px] tablet:max-desktop:flex-col tablet:flex-wrap gap-2 tablet:max-desktop:mb-10 mobile:max-tablet:duration-1000"
style={
width < 640
? {
transform: `translateX(${sliderOffset}px)`,
transition: `${sliderOffset === 0 || sliderOffset === (-width + 80) * 2 ? 0 : 0.4}s`,
}
: {}
}
{...handlers}
>
{width < 640 ? (
order.map((src, index) => (
<img
key={index}
src={src}
className="rounded-lg mobile:max-tablet:min-w-[clamp(280px,100vw-80px,559px)] object-cover pointer-events-none"
alt=""
/>
))
) : (
<>
<img
src="src/assets/train.png"
className="rounded-lg xl:max-desktop-figma:w-[clamp(178px,178px+(100vw-1280px)/320*54,232px)] desktop-figma:w-[calc((100vw-256px)*0.145)] tablet:max-xl:hidden"
alt=""
/>
<img
src="src/assets/dispatcher.png"
className="xl:max-desktop-figma:w-[clamp(178px,178px+(100vw-1280px)/320*54,232px)] desktop-figma:w-[calc((100vw-256px)*0.145)] tablet:max-xl:hidden"
alt=""
/>
<img
src="src/assets/winda.png"
className="xl:max-desktop-figma:w-[clamp(178px,178px+(100vw-1280px)/320*54,232px)] desktop-figma:w-[calc((100vw-256px)*0.145)] tablet:max-xl:hidden"
alt=""
/>
<img
src="src/assets/rzhd.png"
className="xl:max-desktop-figma:w-[clamp(272px,272px+(100vw-1280px)/320*80,352px)] desktop-figma:w-[calc((100vw-256px)*0.22)]"
alt=""
/>
<img
src="src/assets/rzhd2.png"
className="xl:max-desktop-figma:w-[clamp(272px,272px+(100vw-1280px)/320*80,352px)] desktop-figma:w-[calc((100vw-256px)*0.22)]"
alt=""
/>
</>
)}
</div>
</div>
<p className="desktop:hidden mobile:max-tablet:hidden text-[#ffffff] opacity-60 m-text">
модель позволяет производить расчеты характеристик работы,
отслеживать безопасность работы устройств и симулировать внештатные
ситуации.
</p>
</div>
</div>
</div>
);
}
function SimulatorsItem({ text }: { text: string }) {
return (
<li className="text-[#ffffff] l-text bg-[#3D425C4D] rounded-[44px] desktop:px-5 desktop:py-2 mobile:px-4 mobile:py-[6px]">
{text}
</li>
);
}
function ForTeachingTab() {
return (
<div className="desktop:bg-[url('src/assets/mask_group2.png')] bg-[#3D425C4D] bg-no-repeat desktop:p-10 tablet:p-7 mobile:p-5 rounded-xl 2xl:bg-contain bg-[right] desktop:max-2xl:bg-[length:50%]">
<div className='tablet:max-desktop:bg-[url("src/assets/mask_group2.png")] bg-no-repeat bg-right bg-[length:50%] tablet:max-desktop:pb-[55px] mobile:max-desktop:border-b border-[#3D425C] tablet:mb-8 mobile:mb-4'>
<h3 className="text-[#ffffff] font-medium desktop:max-w-[455px] tablet:max-w-[326px] mobile:max-tablet:mb-5 h3">
Интерактивные тренажеры для учебных заведений
</h3>
<img
src="src/assets/mask_group2.png"
className="tablet:hidden"
alt=""
/>
</div>
<div className="flex desktop:flex-col mobile:max-tablet:flex-col desktop:gap-y-6 mobile:gap-y-4 desktop:mb-12 tablet:max-desktop:mb-8 mobile:max-tablet:mb-5 tablet:max-desktop:border-b border-[#3D425C] tablet:max-desktop:pb-8">
<div className="flex gap-x-7 items-start desktop:max-w-[437px] tablet:w-fit tablet:max-desktop:pr-3 mobile:max-tablet:pb-4 mobile:max-tablet:border-b border-[#3D425C]">
<img
src="src/assets/service_icon.svg"
className="mobile:max-desktop:hidden"
alt=""
/>
<div className="desktop:pl-4 tablet:pl-[13px] tablet:border-l border-[#3D425C]">
<div className="flex mobile:max-tablet:items-center tablet:items-start tablet:max-desktop:flex-col gap-x-2 mb-2">
<img
src="src/assets/service_icon.svg"
className="desktop:hidden"
alt=""
/>
<h4 className="text-[#ffffff] font-medium l-text">
cоздание обучающих VR систем
</h4>
</div>
<p className="text-[#ffffff] opacity-60 desktop:font-medium m-text">
Проведение виртуальных практических работ, создание учебных
мастерских и стендов
</p>
</div>
</div>
<div className="flex gap-x-7 items-start desktop:max-w-[437px] tablet:w-fit tablet:max-desktop:pr-3 mobile:max-tablet:pb-4 mobile:max-tablet:border-b border-[#3D425C]">
<img
src="src/assets/service_icon.svg"
className="mobile:max-desktop:hidden"
alt=""
/>
<div className="desktop:pl-4 tablet:pl-[13px] tablet:border-l border-[#3D425C]">
<div className="flex mobile:max-tablet:items-center tablet:items-start tablet:max-desktop:flex-col gap-x-2 mb-2">
<img
src="src/assets/service_icon.svg"
className="desktop:hidden"
alt=""
/>
<h4 className="text-[#ffffff] font-medium l-text">
cоздание VR лабораторий
</h4>
</div>
<p className="text-[#ffffff] opacity-60 desktop:font-medium m-text">
Тренажер для проведения лабораториных работ позволит избежать
поломки оборудования, а также экономить на расходных средствах
</p>
</div>
</div>
</div>
<h4 className="text-[#ffffff] font-medium desktop:max-w-[408px] l-text">
Оснащение учебных классов и центров всем необходимым для современного
обучения под «ключ»
</h4>
</div>
);
}
+1 -1
View File
@@ -4,7 +4,7 @@ import { Title } from '../../ui/Title';
export function Teaching() {
return (
<div className="desktop:py-[70px] desktop:px-10 tablet:max-desktop:px-6 mobile:max-tablet:px-4 mobile:max-desktop:py-14 desktop:flex gap-x-4">
<Title className="mobile:max-desktop:hidden mb-8 desktop:sticky top-0 h-fit">
<Title className="mobile:max-desktop:hidden mb-8 desktop:sticky top-14 h-fit">
<span
className="bg-text-gradient bg-gradient-to-r from-[#798FFF] to-[#D375FF]"
style={{
+23 -4
View File
@@ -1,4 +1,6 @@
import { useHover } from 'usehooks-ts';
import { Title } from '../../ui/Title';
import { useRef } from 'react';
export function Trainings() {
return (
@@ -20,7 +22,7 @@ export function Trainings() {
</span>
, основываясь на специфике вашего тренировочного процесса
</Title>
<div className="desktop:pl-[257px]">
<div className="desktop:max-desktop-figma:pl-[16vw] desktop-figma:pl-[256px]">
<TrainingsFeature
order="[01]"
src="src/assets/vr_1.png"
@@ -55,9 +57,15 @@ function TrainingsFeature({
src: string;
order: string;
}) {
const ref = useRef<HTMLDivElement>(null);
const hovered = useHover(ref);
return (
<div className="tablet:flex items-stretch justify-between tablet:py-10 desktop:max-desktop-figma:max-w-[clamp(557px,557px+(100vw-1024px)/576*576,1133px)] desktop-figma:w-[70.8vw] border-b border-[#3D425C] first:border-t mobile:max-tablet:pt-5">
<div className="tablet:flex flex-col justify-between mobile:max-tablet:mb-[42px] tablet-figma:max-w-[495px]">
<div
ref={ref}
className="desktop:first:h-[200px] desktop:last:h-[200px] desktop:h-[176px] tablet:flex items-stretch justify-between tablet:py-10 desktop:max-desktop-figma:max-w-[clamp(557px,557px+(100vw-1024px)/576*576,1133px)] desktop-figma:w-[70.8vw] border-b border-[#3D425C] first:border-t mobile:max-tablet:pt-5"
>
<div className="tablet:flex flex-col gap-y-4 mobile:max-tablet:mb-[42px] tablet-figma:max-w-[43.7%]">
<h3 className="font-medium text-[#ffffff] mobile:max-tablet:mb-2 h3">
{title}
</h3>
@@ -68,7 +76,18 @@ function TrainingsFeature({
<img src={src} alt="" className="w-[50vw]" />
</div>
<div className="tablet-figma:flex mobile:max-tablet-figma:hidden">
<img src={src} alt="" className="min-w-[calc(280/708*100%)]" />
{hovered && (
<img
src={src}
alt=""
className="relative self-center w-[27vw] -my-10 h-[calc(27vw*0.6)] mobile:max-desktop:hidden"
/>
)}
<img
src={src}
alt=""
className="self-center w-[27vw] h-[calc(27vw*0.6)] desktop:hidden"
/>
<p className="text-[#52587A] desktop:font-medium m-text">{order}</p>
</div>
<img
+16
View File
@@ -0,0 +1,16 @@
export function AsteriskIcon() {
return (
<svg
width="12"
height="13"
viewBox="0 0 12 13"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4.81534 12.2727L4.9858 7.58523L1.02273 10.0994L0 8.30966L4.17614 6.13636L0 3.96307L1.02273 2.1733L4.9858 4.6875L4.81534 0H6.8608L6.69034 4.6875L10.6534 2.1733L11.6761 3.96307L7.5 6.13636L11.6761 8.30966L10.6534 10.0994L6.69034 7.58523L6.8608 12.2727H4.81534Z"
fill="white"
/>
</svg>
);
}
@@ -0,0 +1,37 @@
function CheckGradientIcon({ className = '' }: { className?: string }) {
return (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<g id="Icon/Check">
<path
id="Vector 1836 (Stroke)"
fillRule="evenodd"
clipRule="evenodd"
d="M26.3298 9.78103C26.819 10.3314 26.7694 11.1742 26.2191 11.6634L14.2191 22.3301C13.6914 22.7991 12.8896 22.7755 12.3904 22.2763L5.72378 15.6097C5.20308 15.089 5.20308 14.2447 5.72378 13.724C6.24448 13.2033 7.0887 13.2033 7.60939 13.724L13.3871 19.5017L24.4474 9.6703C24.9978 9.18107 25.8406 9.23065 26.3298 9.78103Z"
fill="url(#paint0_linear_53_10278)"
/>
</g>
<defs>
<linearGradient
id="paint0_linear_53_10278"
x1="5.33325"
y1="32.1907"
x2="29.4088"
y2="29.927"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.167052" stopColor="#798FFF" />
<stop offset="0.963542" stopColor="#D375FF" />
</linearGradient>
</defs>
</svg>
);
}
export default CheckGradientIcon;
+20
View File
@@ -0,0 +1,20 @@
export function Close2Icon({ className = '' }: { className?: string }) {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
d="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"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);
}
+35
View File
@@ -0,0 +1,35 @@
function LoaderIcon({ className = '' }: { className?: string }) {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<g id="Icon/Circle">
<path
id="Ellipse 221"
d="M18.9999 12C19.5523 12 20.0064 11.5505 19.9376 11.0025C19.745 9.46994 19.1116 8.01808 18.1039 6.82871C16.8797 5.38372 15.1826 4.41989 13.3144 4.10872C11.4463 3.79755 9.52839 4.15922 7.90189 5.12938C6.27539 6.09953 5.04582 7.61525 4.43194 9.40685C3.81806 11.1985 3.85968 13.1497 4.54941 14.9135C5.23914 16.6773 6.53224 18.1392 8.19863 19.0391C9.86502 19.9391 11.7966 20.2186 13.6498 19.828C15.1751 19.5066 16.5658 18.7483 17.6578 17.6559C18.0483 17.2653 17.9652 16.6317 17.529 16.2929C17.0927 15.9542 16.4693 16.0409 16.0629 16.4149C15.2735 17.1413 14.2989 17.6472 13.2373 17.8709C11.8475 18.1638 10.3988 17.9541 9.14904 17.2792C7.89928 16.6043 6.92948 15.5079 6.4122 14.1851C5.89491 12.8623 5.86369 11.3989 6.32409 10.0552C6.78449 8.71152 7.70665 7.57476 8.92649 6.84716C10.1463 6.11956 11.5847 5.84832 12.9858 6.08169C14.3869 6.31506 15.6597 7.03791 16.5778 8.12163C17.2791 8.94938 17.7387 9.94667 17.9167 11.0045C18.0083 11.5492 18.4476 12 18.9999 12Z"
fill="url(#paint0_angular_0_1327)"
/>
</g>
<defs>
<radialGradient
id="paint0_angular_0_1327"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(12 12) rotate(45) scale(7.9196)"
>
<stop offset="0.874517" stopColor="white" />
<stop offset="0.982613" stopColor="white" stopOpacity="0" />
</radialGradient>
</defs>
</svg>
);
}
export default LoaderIcon;
+24
View File
@@ -0,0 +1,24 @@
function SendIcon({ className = '' }: { className?: string }) {
return (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<g id="Icon/Send">
<path
id="Vector 164 (Stroke)"
fillRule="evenodd"
clipRule="evenodd"
d="M25.2649 8.4215C25.432 7.92033 25.2874 7.36779 24.8963 7.01269C24.5051 6.65759 23.9412 6.56689 23.4585 6.78145L6.56115 14.2914C4.82296 15.0639 5.043 17.5979 6.88835 18.0593L10.0482 18.8492C10.6608 19.0024 11.3097 18.8572 11.7987 18.4577L19.8248 11.8996C20.0112 11.7473 20.2583 11.9935 20.1068 12.1805L14.0759 19.62C13.5817 20.2296 13.4898 21.0719 13.8407 21.7738L15.8654 25.8233C16.6623 27.417 18.9882 27.2516 19.5517 25.5613L25.2649 8.4215Z"
fill="white"
/>
</g>
</svg>
);
}
export default SendIcon;
+2 -2
View File
@@ -10,10 +10,10 @@ body {
@layer components {
.h1 {
@apply -tracking-[.02em] leading-[90%] desktop-figma:text-[clamp(96px,6vw,112px)] desktop:max-desktop-figma:text-[clamp(76px,76px+(100vw-1024px)/576*20,96px)] tablet-figma:max-desktop:text-[clamp(64px,64px+(100vw-768px)/256*16,80px)] tablet:max-tablet-figma:text-[clamp(56px,56px+(100vw-640px)/128*8,64px)] mobile:max-tablet:text-[clamp(40px,40px+(100vw-360px)/280*4,44px)];
@apply -tracking-[.02em] leading-[90%] desktop-figma:text-[clamp(96px,6vw,128px)] desktop:max-desktop-figma:text-[clamp(76px,76px+(100vw-1024px)/576*20,96px)] tablet-figma:max-desktop:text-[clamp(64px,64px+(100vw-768px)/256*16,80px)] tablet:max-tablet-figma:text-[clamp(56px,56px+(100vw-640px)/128*8,64px)] mobile:max-tablet:text-[clamp(40px,40px+(100vw-360px)/280*4,44px)];
}
.h2 {
@apply -tracking-[.02em] desktop:leading-[90%] mobile:max-desktop:leading-[100%] desktop-figma:text-[clamp(64px,4vw,72px)] desktop:max-desktop-figma:text-[clamp(56px,56px+(100vw-1024px)/576*8,64px)] tablet-figma:max-desktop:text-[clamp(40px,40px+(100vw-768px)/256*12,52px)] tablet:max-tablet-figma:text-[clamp(32px,32px+(1000vw-640px)/128*8,40px)] mobile:max-tablet:text-[clamp(28px,28px+(100vw-360px)/280*4,32px)];
@apply -tracking-[.02em] desktop:leading-[90%] mobile:max-desktop:leading-[100%] desktop-figma:text-[clamp(64px,4vw,80px)] desktop:max-desktop-figma:text-[clamp(56px,56px+(100vw-1024px)/576*8,64px)] tablet-figma:max-desktop:text-[clamp(40px,40px+(100vw-768px)/256*12,52px)] tablet:max-tablet-figma:text-[clamp(32px,32px+(1000vw-640px)/128*8,40px)] mobile:max-tablet:text-[clamp(28px,28px+(100vw-360px)/280*4,32px)];
}
.h3 {
+29 -17
View File
@@ -1,28 +1,40 @@
import { useInView, motion } from 'framer-motion';
import { useRef } from 'react';
import { motion, useScroll, useMotionValueEvent } from 'framer-motion';
import { useRef, useState } from 'react';
export default function AppearanceText({
text,
opacity,
duration,
}: {
text: string;
opacity: number;
duration: number;
}) {
function AppearanceItem({ text }: { text: string }) {
const ref = useRef<HTMLSpanElement>(null);
const isInView = useInView(ref);
const [isShowed, setIsShowed] = useState(false);
const { scrollY } = useScroll();
useMotionValueEvent(scrollY, 'change', latest => {
setIsShowed(
latest >= (ref.current?.offsetTop ?? 0) - (window.innerHeight / 3) * 2,
);
});
return (
<motion.span
ref={ref}
style={{ transition: duration + 's' }}
className={
// `duration-[${duration}s]` +
isInView ? ` opacity-${opacity}` : ' opacity-0'
}
className={'duration-200 ' + (isShowed ? 'opacity-100' : 'opacity-0')}
>
{text}
</motion.span>
);
}
export function AppearanceText({
splits,
className = '',
}: {
splits: string[];
className?: string;
}) {
return (
<h3 className={'text-[#ffffff] font-medium h3 ' + className}>
{splits.map(text => (
<AppearanceItem key={text} text={text} />
))}
</h3>
);
}
+40
View File
@@ -0,0 +1,40 @@
import { ReactNode } from 'react';
interface ButtonProps {
children: ReactNode;
icon?: JSX.Element;
color?: 'primary' | 'secondary';
width?: 'fit' | 'full';
disabled?: boolean;
className?: string;
onClick?: () => void;
}
function Button({
children,
color = 'primary',
icon,
width = 'fit',
disabled = false,
className,
onClick,
}: ButtonProps) {
return (
<button
disabled={disabled}
onClick={onClick}
className={`group relative px-6 py-2 rounded-full min-w-fit ${
(color === 'primary' ? 'bg-gradient' : '') ||
(color === 'secondary' ? 'outline outline-1 outline-[#3D425C]' : '')
} ${
icon ? 'pr-4' : ''
} flex justify-between gap-1 items-center overflow-hidden w-${width} ${className}`}
>
<span className="group-hover:opacity-10 opacity-0 bg-black transition-opacity absolute top-0 left-0 w-full h-full"></span>
<span className="relative font-gilroy font-medium">{children}</span>
<span className="relative ">{icon}</span>
</button>
);
}
export default Button;
+59 -1
View File
@@ -581,6 +581,13 @@
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64"
integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==
"@types/node@^20.14.10":
version "20.14.10"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.10.tgz#a1a218290f1b6428682e3af044785e5874db469a"
integrity sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==
dependencies:
undici-types "~5.26.4"
"@types/prop-types@*":
version "15.7.12"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6"
@@ -593,6 +600,13 @@
dependencies:
"@types/react" "*"
"@types/react-input-mask@^3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@types/react-input-mask/-/react-input-mask-3.0.5.tgz#9fbe9a984b3299419a6071dbf697ac2cae2abd2d"
integrity sha512-vQ1x6ykwjDrDrJZq1zw5/uQ+nqGHUV6bWscsVZJ/qsNwNXWxZm7KRBHLJ5k6TQt3MHjhpoYHzPH6FwjVSZODHA==
dependencies:
"@types/react" "*"
"@types/react-router-dom@^5.3.0":
version "5.3.3"
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83"
@@ -1384,6 +1398,13 @@ inherits@2:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
dependencies:
loose-envify "^1.0.0"
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
@@ -1488,6 +1509,11 @@ keyv@^4.5.3:
dependencies:
json-buffer "3.0.1"
ky@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/ky/-/ky-1.4.0.tgz#68b4a71eccfb4177199fe6ee2d5041b50bb41931"
integrity sha512-tPhhoGUiEiU/WXR4rt8klIoLdnTtyu+9jVKHd/wauEjYud32jyn63mzKWQweaQrHWxBQtYoVtdcEnYX1LosnFQ==
levn@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
@@ -1518,12 +1544,17 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
loose-envify@^1.1.0, loose-envify@^1.4.0:
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -1811,6 +1842,14 @@ react-dom@^18.3.1:
loose-envify "^1.1.0"
scheduler "^0.23.2"
react-input-mask@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/react-input-mask/-/react-input-mask-2.0.4.tgz#9ade5cf8196f4a856dbf010820fe75a795f3eb14"
integrity sha512-1hwzMr/aO9tXfiroiVCx5EtKohKwLk/NT8QlJXHQ4N+yJJFyUuMT+zfTpLBwX/lK3PkuMlievIffncpMZ3HGRQ==
dependencies:
invariant "^2.2.4"
warning "^4.0.2"
react-is@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@@ -2152,6 +2191,11 @@ typescript@^5.2.2:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.2.tgz#c26f023cb0054e657ce04f72583ea2d85f8d0507"
integrity sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==
undici-types@~5.26.4:
version "5.26.5"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
update-browserslist-db@^1.0.16:
version "1.0.16"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz#f6d489ed90fb2f07d67784eb3f53d7891f736356"
@@ -2172,6 +2216,13 @@ use-sync-external-store@1.2.0:
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
usehooks-ts@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/usehooks-ts/-/usehooks-ts-3.1.0.tgz#156119f36efc85f1b1952616c02580f140950eca"
integrity sha512-bBIa7yUyPhE1BCc0GmR96VU/15l/9gP1Ch5mYdLcFBaFGQsdmXkvjV0TtOqW1yUd6VjIwDunm+flSciCQXujiw==
dependencies:
lodash.debounce "^4.0.8"
util-deprecate@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
@@ -2188,6 +2239,13 @@ vite@^5.3.1:
optionalDependencies:
fsevents "~2.3.3"
warning@^4.0.2:
version "4.0.3"
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
dependencies:
loose-envify "^1.0.0"
which@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"