updated slides, upgrade phone input with mask and country flag, started tablet etc
@@ -10,8 +10,8 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"countries-phone-masks": "^1.1.0",
|
||||
"framer-motion": "^11.3.31",
|
||||
"intl-tel-input": "^24.3.6",
|
||||
"ky": "^1.7.1",
|
||||
"libphonenumber-js": "^1.11.7",
|
||||
"react": "^18.3.1",
|
||||
@@ -20,6 +20,7 @@
|
||||
"react-phone-number-input": "^3.4.5",
|
||||
"react-router-dom": "^6.26.1",
|
||||
"react-swipeable": "^7.0.1",
|
||||
"react-usestateref": "^1.0.9",
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"zustand": "^4.5.5"
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 3.1 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1019 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
@@ -1,71 +1,7 @@
|
||||
import examples from 'libphonenumber-js/mobile/examples';
|
||||
import {
|
||||
formatPhoneNumber,
|
||||
getCountries,
|
||||
getCountryCallingCode,
|
||||
} from 'react-phone-number-input';
|
||||
import ReactInputMask from 'react-input-mask';
|
||||
import { FormEvent, useEffect, useRef, useState } from 'react';
|
||||
import { Title } from './ui/Title';
|
||||
import { api } from '../api';
|
||||
import { Button } from './ui/Button';
|
||||
import { ClassNameWrapper } from '../hocs/ClassNameWrapper';
|
||||
import { LoaderIcon } from './icons/LoaderIcon';
|
||||
import { ChevronUpIcon } from './icons/ChevronUpIcon';
|
||||
import { ChevronDownIcon } from './icons/ChevronDownIcon';
|
||||
import { ArrowRightIcon } from './icons/ArrowRightIcon';
|
||||
import { CountryCode, getExampleNumber } from 'libphonenumber-js';
|
||||
import { FeedbackForm } from './Layout/FeedbackForm';
|
||||
|
||||
export function Form() {
|
||||
const [name, setName] = useState('');
|
||||
const [[phoneCode, country], setPhoneCodeAndCountry] = useState<
|
||||
[string, string]
|
||||
>(['', '']);
|
||||
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);
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
if (error instanceof Error) {
|
||||
alert(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-6 space-y-20">
|
||||
<Title className="max-w-[58vw]">
|
||||
@@ -74,130 +10,9 @@ export function Form() {
|
||||
<span className="text-gradient">Давайте обсудим детали.</span>
|
||||
</Title>
|
||||
<div className="relative">
|
||||
<form onSubmit={handleSubmit} className="space-y-12 max-w-[66vw]">
|
||||
<div className="space-y-6">
|
||||
<div className="flex gap-x-4 items-start">
|
||||
<div className="w-full">
|
||||
<label
|
||||
className="m-text text-[#9299BD] select-none"
|
||||
htmlFor="name"
|
||||
>
|
||||
Имя
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
id="name"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder="Ваше имя"
|
||||
className="bg-transparent border-b border-[#3D425C] focus:border-white py-4 rounded-none outline-none transition-all w-full placeholder:h4 placeholder:font-medium placeholder:select-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
<label
|
||||
className="m-text text-[#9299BD] select-none"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email*
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
id="email"
|
||||
type="text"
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
placeholder="Ваш email"
|
||||
className="bg-transparent border-b border-[#3D425C] focus:border-white py-4 rounded-none outline-none transition-all w-full placeholder:h4 placeholder:font-medium placeholder:select-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
<label
|
||||
className="m-text text-[#9299BD] select-none"
|
||||
htmlFor="tel"
|
||||
>
|
||||
Телефон
|
||||
</label>
|
||||
<div className="flex gap-x-3 py-4 border-[#3D425C] relative">
|
||||
<SelectPhoneCode
|
||||
currentPhoneCode={phoneCode}
|
||||
onClick={setPhoneCodeAndCountry}
|
||||
/>
|
||||
<div className="border-l border-[#3D425C]" />
|
||||
<ReactInputMask
|
||||
required
|
||||
type="tel"
|
||||
id="tel"
|
||||
mask={
|
||||
getExampleNumber(country as CountryCode, examples)
|
||||
?.formatNational
|
||||
}
|
||||
maskChar={null}
|
||||
value={phone}
|
||||
placeholder={formatPhoneNumber(examples[country] as string)}
|
||||
onChange={e => setPhone(e.target.value)}
|
||||
className="bg-transparent rounded-none outline-none transition-all w-full placeholder:h4 placeholder:font-medium placeholder:select-none peer"
|
||||
/>
|
||||
<div className="bottom-0 absolute w-full border-b border-[#3D425C] peer-focus:border-white -mb-px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
className="m-text text-[#9299BD] select-none"
|
||||
htmlFor="description"
|
||||
>
|
||||
Задача
|
||||
</label>
|
||||
<textarea
|
||||
ref={textAreaRef}
|
||||
id="description"
|
||||
placeholder="Опишите вашу задачу"
|
||||
value={description}
|
||||
rows={1}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
className="bg-transparent border-b py-4 focus:border-white max-h-[300px] h-auto rounded-none border-[#3D425C] resize-none outline-none transition-all w-full placeholder:h4 placeholder:font-medium placeholder:select-none focus:overflow-y-scroll overflow-hidden"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 max-w-[25vw]">
|
||||
<Button
|
||||
width="full"
|
||||
disabled={isLoading}
|
||||
className="p-2 pl-8"
|
||||
icon={
|
||||
isLoading ? (
|
||||
<ClassNameWrapper
|
||||
element={<LoaderIcon />}
|
||||
className="relative w-5 h-5 animate-spin"
|
||||
/>
|
||||
) : (
|
||||
<div className="p-2 rounded-full bg-white">
|
||||
<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>
|
||||
<FeedbackForm />
|
||||
<div className="absolute bottom-0 -right-[min(136px,calc(136/1600*100vw))] animate-[spin_10s_linear_infinite]">
|
||||
<div className="relative w-[calc(512/1600*100vw)] aspect-square flex justify-center transition-all duration-700 hover:w-[calc(446/1600*100vw)] origin-center">
|
||||
<div className="relative w-[calc(512/1600*100vw)] aspect-square flex justify-center transition-all duration-500 hover:w-[calc(446/1600*100vw)] origin-center">
|
||||
<div className="w-[calc(116/1600*100vw)] flex flex-col justify-between h-full absolute">
|
||||
<img src="/src/assets/form/1_1.png" alt="" />
|
||||
<img src="/src/assets/form/1_2.png" alt="" />
|
||||
@@ -244,51 +59,3 @@ export function Form() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectPhoneCode({
|
||||
currentPhoneCode,
|
||||
onClick,
|
||||
}: {
|
||||
currentPhoneCode: string;
|
||||
onClick: ([phoneCode, country]: [string, string]) => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(getCountries().map(country => getCountryCallingCode(country)));
|
||||
}, []);
|
||||
|
||||
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]">
|
||||
{getCountries()
|
||||
.map(country => [getCountryCallingCode(country), country])
|
||||
.filter(([phonecode]) => phonecode !== currentPhoneCode)
|
||||
.map(([phoneCode, country]) => (
|
||||
<p
|
||||
key={phoneCode}
|
||||
className="h4 cursor-pointer hover:bg-[#3D425C] py-1"
|
||||
onClick={() => {
|
||||
onClick([phoneCode, country]);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{phoneCode}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
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';
|
||||
import { getExampleNumber } from 'libphonenumber-js';
|
||||
import examples from 'libphonenumber-js/mobile/examples';
|
||||
|
||||
export function FeedbackForm({
|
||||
inModal = false,
|
||||
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' : 'space-y-12 max-w-[66vw]'}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div
|
||||
className={
|
||||
'grid gap-x-4 items-start ' + (inModal ? 'gap-y-6' : 'grid-cols-3')
|
||||
}
|
||||
>
|
||||
<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="bg-transparent rounded-none outline-none transition-all w-full 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 max-w-[25vw]">
|
||||
<Button
|
||||
width="full"
|
||||
disabled={isLoading}
|
||||
className="p-2 pl-8"
|
||||
icon={
|
||||
isLoading ? (
|
||||
<ClassNameWrapper
|
||||
element={<LoaderIcon />}
|
||||
className="relative w-5 h-5 animate-spin"
|
||||
/>
|
||||
) : (
|
||||
<div className="p-2 rounded-full bg-white">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
FeedbackForm.displayName = 'FeedbackForm';
|
||||
@@ -4,16 +4,30 @@ import { Button } from '../ui/Button';
|
||||
import { ClassNameWrapper } from '../../hocs/ClassNameWrapper';
|
||||
import { useModalStore } from '../../stores/modalStore';
|
||||
import { ModalWithForm } from './ModalWithForm';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useRef, useState } from 'react';
|
||||
import { BurgerIcon } from '../icons/BurgerIcon';
|
||||
import { CloseIcon } from '../icons/CloseIcon';
|
||||
import { useOnClickOutside } from 'usehooks-ts';
|
||||
import { useLanguageStore } from '../../stores/languageStore';
|
||||
|
||||
export function Header() {
|
||||
const { setModal } = useModalStore();
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const menuBtnRef = useRef<HTMLButtonElement>(null);
|
||||
useOnClickOutside<HTMLDivElement | HTMLButtonElement>(
|
||||
[menuRef, menuBtnRef],
|
||||
() => setMenuOpen(false),
|
||||
);
|
||||
|
||||
return (
|
||||
<header className="lg:px-6 flex items-center h-16 border-b border-[#3D425C] bg-[#14161F]">
|
||||
<header className="lg:px-6 sm:px-4 flex max-lg:justify-between items-center h-16 border-b border-[#3D425C] bg-[#14161F]">
|
||||
<Link to={'/'}>
|
||||
<ClassNameWrapper element={<Logo />} className="h-10" />
|
||||
</Link>
|
||||
<nav className="mx-auto self-stretch flex">
|
||||
<nav className="mx-auto self-stretch flex max-lg:hidden">
|
||||
{[
|
||||
{ path: '/#products', text: 'Продукты' },
|
||||
{ path: '/#devices', text: 'Оборудование' },
|
||||
@@ -23,12 +37,59 @@ export function Header() {
|
||||
<HashLink key={link.path} {...link} />
|
||||
))}
|
||||
</nav>
|
||||
<div className="lg:-mr-6 sm:-mr-4 flex h-full">
|
||||
<Button
|
||||
className="-mr-6 rounded-none h-full px-10"
|
||||
className="rounded-none h-full px-10"
|
||||
onClick={() => setModal(<ModalWithForm />)}
|
||||
>
|
||||
Отправить заявку
|
||||
Оставить заявку
|
||||
</Button>
|
||||
<button
|
||||
ref={menuBtnRef}
|
||||
className="p-3 mr-0 aspect-square h-full lg:hidden"
|
||||
onClick={() => setMenuOpen(prev => !prev)}
|
||||
>
|
||||
<ClassNameWrapper
|
||||
element={!menuOpen ? <BurgerIcon /> : <CloseIcon />}
|
||||
className={'mx-auto'}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, visibility: 'hidden' }}
|
||||
animate={{
|
||||
opacity: +menuOpen,
|
||||
visibility: menuOpen ? 'visible' : 'hidden',
|
||||
}}
|
||||
transition={{ duration: 0.2 }}
|
||||
ref={menuRef}
|
||||
onClick={() => setMenuOpen(false)}
|
||||
className={
|
||||
'absolute z-50 w-full lg:hidden sm:max-[1350px]:max-w-[340px] right-0 top-16' +
|
||||
(menuOpen ? ' shadow-[0_0_0_9999px_rgba(0,0,0,.4)]' : '')
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<HashLink text="Продукты" path={'#products'} />
|
||||
<HashLink text="Оборудование" path={'#devices'} />
|
||||
<HashLink text="Проекты" path={'#projects'} />
|
||||
<HashLink text="Контакты" path={'#contacts'} />
|
||||
</div>
|
||||
<div className="grid grid-cols-[2fr_1fr_1fr] sm:grid-cols-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setMenuOpen(false);
|
||||
setModal(<ModalWithForm />);
|
||||
}}
|
||||
width="full"
|
||||
className="sm:hidden font-semibold btn-text rounded-none outline-none"
|
||||
>
|
||||
Оставить заявку
|
||||
</Button>
|
||||
<ChooseLang currentLang="RU" />
|
||||
<ChooseLang currentLang="EN" />
|
||||
</div>
|
||||
</motion.div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -36,10 +97,28 @@ export function Header() {
|
||||
function HashLink({ path, text }: { path: string; text: string }) {
|
||||
return (
|
||||
<Link
|
||||
className="border-l last:border-r border-[#3D425C] px-10 self-stretch text-[#9299BD] content-center hover:bg-[#3D425C]"
|
||||
className="border-l lg:last:border-r border-[#3D425C] px-10 self-stretch lg:text-[#9299BD] content-center hover:bg-[#3D425C] max-lg:flex max-lg:py-6 btn-text bg-[#14161F] w-full max-lg:[&:not(:last-child)]:border-b"
|
||||
to={path}
|
||||
>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function ChooseLang({ currentLang }: { currentLang: 'RU' | 'EN' }) {
|
||||
const { setLang, lang } = useLanguageStore();
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => setLang(currentLang)}
|
||||
className={
|
||||
'min-h-[72px] w-full h-full btn-text font-semibold bg-[#14161F] lg:hover:bg-[#3D425C] border active:bg-[#14161F] outline-none ' +
|
||||
(lang === currentLang
|
||||
? '[border-image:linear-gradient(to_right,#798FFF,#D375FF)_3]'
|
||||
: 'border-[#3D425C]')
|
||||
}
|
||||
>
|
||||
{currentLang}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function ModalMenu() {
|
||||
return <div></div>;
|
||||
}
|
||||
@@ -1,68 +1,15 @@
|
||||
import { api } from '../../api';
|
||||
import { phoneCodes } from '../../consts/phoneCodes';
|
||||
import { ClassNameWrapper } from '../../hocs/ClassNameWrapper';
|
||||
import { useModalStore } from '../../stores/modalStore';
|
||||
import { PhoneCode } from '../../types/PhoneCode';
|
||||
import { Button } from '../ui/Button';
|
||||
import { FormEvent, useEffect, useRef, useState } from 'react';
|
||||
import ReactInputMask from 'react-input-mask';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ArrowRightIcon } from '../icons/ArrowRightIcon';
|
||||
import { ChevronDownIcon } from '../icons/ChevronDownIcon';
|
||||
import { ChevronUpIcon } from '../icons/ChevronUpIcon';
|
||||
import { CloseIcon } from '../icons/CloseIcon';
|
||||
import { LoaderIcon } from '../icons/LoaderIcon';
|
||||
import { MailIcon } from '../icons/MailIcon';
|
||||
import { FeedbackForm } from './FeedbackForm';
|
||||
|
||||
export function ModalWithForm() {
|
||||
const { setModal } = useModalStore();
|
||||
const [name, setName] = useState('');
|
||||
const [phoneCode, setPhoneCode] = useState<PhoneCode>('+7');
|
||||
const [phone, setPhone] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSend, setIsSend] = 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();
|
||||
|
||||
setIsSend(true);
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
if (error instanceof Error) {
|
||||
alert(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
const [sent, setSent] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (e: KeyboardEvent) => {
|
||||
@@ -71,12 +18,13 @@ export function ModalWithForm() {
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', listener);
|
||||
|
||||
return () => document.removeEventListener('keydown', listener);
|
||||
}, [setModal]);
|
||||
|
||||
return (
|
||||
<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 ? (
|
||||
{!sent ? (
|
||||
<div className="space-y-8">
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="font-medium accent">Оставьте заявку</p>
|
||||
@@ -87,124 +35,10 @@ export function ModalWithForm() {
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="space-y-6">
|
||||
<hr className="border-[#3D425C]" />
|
||||
|
||||
<div>
|
||||
<label
|
||||
className="m-text text-[#9299BD] select-none"
|
||||
htmlFor="name"
|
||||
>
|
||||
Имя
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
id="name"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder="Ваше имя"
|
||||
className="bg-transparent border-b border-[#3D425C] focus:border-white py-4 rounded-none outline-none transition-all w-full placeholder:h4 placeholder:font-medium placeholder:select-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
className="m-text text-[#9299BD] select-none"
|
||||
htmlFor="tel"
|
||||
>
|
||||
Телефон
|
||||
</label>
|
||||
<div className="flex gap-x-3 py-2 border-[#3D425C] relative">
|
||||
<SelectPhoneCode
|
||||
currentPhoneCode={phoneCode}
|
||||
onClick={setPhoneCode}
|
||||
/>
|
||||
<div className="border-l border-[#3D425C]" />
|
||||
<ReactInputMask
|
||||
required
|
||||
type="tel"
|
||||
id="tel"
|
||||
mask={'(999) 99 999 99'}
|
||||
maskChar={null}
|
||||
value={phone}
|
||||
placeholder="(900) 000 00 00"
|
||||
onChange={e => setPhone(e.target.value)}
|
||||
className="bg-transparent rounded-none outline-none transition-all w-full placeholder:h4 placeholder:font-medium placeholder:select-none peer"
|
||||
/>
|
||||
<div className="bottom-0 absolute w-full border-b border-[#3D425C] peer-focus:border-white -mb-2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
className="m-text text-[#9299BD] select-none"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email*
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
id="email"
|
||||
type="text"
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
placeholder="Ваш email"
|
||||
className="bg-transparent border-b border-[#3D425C] focus:border-white py-4 rounded-none outline-none transition-all w-full placeholder:h4 placeholder:font-medium placeholder:select-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
className="m-text text-[#9299BD] select-none"
|
||||
htmlFor="description"
|
||||
>
|
||||
Задача
|
||||
</label>
|
||||
<textarea
|
||||
ref={textAreaRef}
|
||||
id="description"
|
||||
placeholder="Опишите вашу задачу"
|
||||
value={description}
|
||||
rows={1}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
className="bg-transparent border-b py-4 focus:border-white max-h-[300px] h-auto rounded-none border-[#3D425C] resize-none outline-none transition-all w-full placeholder:h4 placeholder:font-medium placeholder:select-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
width="full"
|
||||
disabled={isLoading}
|
||||
className="py-5 px-6 mt-[213px]"
|
||||
icon={
|
||||
isLoading ? (
|
||||
<ClassNameWrapper
|
||||
element={<LoaderIcon />}
|
||||
className="relative w-6 h-6 animate-spin"
|
||||
/>
|
||||
) : (
|
||||
<ClassNameWrapper
|
||||
element={<MailIcon />}
|
||||
className="relative w-6 h-6"
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
Отправить
|
||||
</Button>
|
||||
<p className="m-caption mt-6 text-center text-[#52587A]">
|
||||
Нажимая кнопку отправить, вы принимаете
|
||||
<span className="text-gradient">
|
||||
{' '}
|
||||
условия использования и политику конфиденциальности
|
||||
</span>
|
||||
</p>
|
||||
</form>
|
||||
<FeedbackForm inModal send={() => setSent(true)} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="">
|
||||
<div>
|
||||
<div className="space-y-8">
|
||||
<h2 className="h2 font-medium">Спасибо за отправку заявки!</h2>
|
||||
<p className="m-text">
|
||||
@@ -230,46 +64,3 @@ export function ModalWithForm() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,15 +4,16 @@ import { useLocation } from 'react-router-dom';
|
||||
export function ScrollToHashElement() {
|
||||
const { hash } = useLocation();
|
||||
|
||||
const hashElement = document.getElementById(hash.slice(1));
|
||||
|
||||
useEffect(() => {
|
||||
const hashElement = document.getElementById(hash.slice(1));
|
||||
console.log('scrolling to', hashElement);
|
||||
if (hashElement)
|
||||
hashElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
inline: 'nearest',
|
||||
block: 'start',
|
||||
});
|
||||
}, [hashElement]);
|
||||
}, [hash]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export function Layout() {
|
||||
<div className="overflow-clip">
|
||||
<ScrollToHashElement />
|
||||
<Header />
|
||||
<main className="lg:px-6 relative">
|
||||
<main className="lg:px-6 sm:px-4 relative">
|
||||
<Ellipse />
|
||||
<Outlet />
|
||||
</main>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Showreel } from './Showreel';
|
||||
|
||||
export function Motivation() {
|
||||
@@ -14,11 +15,15 @@ export function Motivation() {
|
||||
{ solution: 'Интерактивные приложения', count: 21 },
|
||||
{ solution: '3D макеты', count: 14 },
|
||||
].map(({ count, solution }, index) => (
|
||||
<p key={solution} className="h4 font-medium flex gap-x-[7px]">
|
||||
<Link
|
||||
key={solution}
|
||||
className="h4 font-medium flex gap-x-[7px]"
|
||||
to={`#${index === 0 ? 'vr_ar' : index == 1 ? 'interactive' : '3d_makets'}`}
|
||||
>
|
||||
{solution}
|
||||
<sup className="text-xs">{count}</sup>
|
||||
{index !== 2 && <span>/</span>}
|
||||
</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,10 +3,15 @@ import { Title } from './ui/Title';
|
||||
import { promotionFeatures } from '../consts/promotionFeatures';
|
||||
import { useRef } from 'react';
|
||||
import { useHover } from 'usehooks-ts';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { hashes } from '../consts/motivationHashes';
|
||||
|
||||
export function Promotion() {
|
||||
return (
|
||||
<div id="products" className="space-y-20 -mt-20">
|
||||
<div
|
||||
id="products"
|
||||
className="lg:space-y-20 sm:space-y-10 lg:-mt-20 sm:-mt-10"
|
||||
>
|
||||
<Title className="max-w-[calc(1310/1600*100vw)]">
|
||||
Повышаем количество посетителей на стенде,
|
||||
<span className="text-gradient">
|
||||
@@ -16,14 +21,14 @@ export function Promotion() {
|
||||
</Title>
|
||||
<div>
|
||||
{promotionFeatures.map((feature, index) => (
|
||||
<Feature key={feature.title} number={index + 1} {...feature} />
|
||||
<DesktopFeature key={feature.title} number={index + 1} {...feature} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Feature({
|
||||
function DesktopFeature({
|
||||
description,
|
||||
images,
|
||||
number,
|
||||
@@ -34,30 +39,52 @@ function Feature({
|
||||
description: string;
|
||||
images: string[];
|
||||
}) {
|
||||
const { hash } = useLocation();
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const hovered = useHover(ref);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="p-7 border-t border-x border-[#3D425C] last:border-b group hover:bg-[url(/src/assets/promotion/Ellipse.png)] bg-no-repeat bg-center bg-cover"
|
||||
id={hashes.get(number)}
|
||||
className="p-7 border-t border-x border-[#3D425C] last:border-b hover:bg-[url(/src/assets/promotion/Ellipse.png)] bg-no-repeat bg-center bg-cover"
|
||||
>
|
||||
<motion.div
|
||||
className="flex justify-between gap-x-4 [clip-path:polygon(0%_0%,100%_0%,100%_calc(100%+28px),0%_calc(100%+28px))]"
|
||||
transition={{ duration: 0.7 }}
|
||||
animate={hovered ? { height: 424 } : { height: 140 }}
|
||||
animate={
|
||||
hovered || hash.slice(1) === hashes.get(number)
|
||||
? { height: 424 }
|
||||
: { height: 140 }
|
||||
}
|
||||
>
|
||||
<div className="max-w-[calc(540/1600*100vw)]">
|
||||
<div className="mb-6 group-hover:space-y-[220px]">
|
||||
<div
|
||||
className={
|
||||
'mb-6' +
|
||||
(hovered || hash.slice(1) === hashes.get(number)
|
||||
? ' space-y-[220px]'
|
||||
: '')
|
||||
}
|
||||
>
|
||||
<p className="l-text text-[#52587A] font-medium">[0{number}]</p>
|
||||
<p className="h3 font-medium mt-12 group-hover:mt-[220px] transition-all duration-700">
|
||||
<p
|
||||
className={
|
||||
'h3 font-medium mt-12 transition-all duration-700' +
|
||||
(hovered || hash.slice(1) === hashes.get(number)
|
||||
? ' mt-[220px]'
|
||||
: '')
|
||||
}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
</div>
|
||||
<motion.p
|
||||
className="h4 opacity-60 font-medium"
|
||||
animate={
|
||||
hovered
|
||||
hovered || hash.slice(1) === hashes.get(number)
|
||||
? { opacity: 0.6, transition: { delay: 0.5 } }
|
||||
: { opacity: 0 }
|
||||
}
|
||||
@@ -69,7 +96,8 @@ function Feature({
|
||||
className="flex gap-x-2 items-start flex-1 justify-end"
|
||||
transition={{ duration: 1 }}
|
||||
animate={{
|
||||
translateX: hovered
|
||||
translateX:
|
||||
hovered || hash.slice(1) === hashes.get(number)
|
||||
? (images.length - 1) * 240 + (images.length - 2) * 8
|
||||
: 0,
|
||||
}}
|
||||
@@ -82,7 +110,13 @@ function Feature({
|
||||
alt=""
|
||||
transition={{ duration: 0.5 }}
|
||||
className="transition-[height] h-full"
|
||||
animate={{ maxHeight: hovered && index === 0 ? 424 : 140 }}
|
||||
animate={{
|
||||
maxHeight:
|
||||
(hovered || hash.slice(1) === hashes.get(number)) &&
|
||||
index === 0
|
||||
? 424
|
||||
: 140,
|
||||
}}
|
||||
/>
|
||||
</AnimatePresence>
|
||||
))}
|
||||
@@ -91,3 +125,15 @@ function Feature({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Feature({
|
||||
title,
|
||||
description,
|
||||
images,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
images: string[];
|
||||
}) {
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
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';
|
||||
|
||||
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 w-1/3">
|
||||
<button
|
||||
className="flex gap-x-1 justify-between items-center relative"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
setOpen(prev => !prev);
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={countries.find(c => c.iso === currentCountry)?.flag}
|
||||
className="w-6"
|
||||
alt=""
|
||||
/>
|
||||
<p className="h4">{currentPhoneCode}</p>
|
||||
{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-6"
|
||||
/>
|
||||
<p className="flex-1 h4 cursor-pointer py-1">{phoneCode}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { FullScreenIcon } from './icons/FullScreenIcon';
|
||||
|
||||
export function Showreel() {
|
||||
return (
|
||||
<div className="aspect-[1552/616] [background:linear-gradient(rgba(0,0,0,0.2),rgba(0,0,0,0.2)),center/cover_url(src/assets/motivation/Showreel.png)_no-repeat] flex justify-center items-center">
|
||||
<div className="lg:aspect-[1552/616] sm:aspect-[720/440] [background:linear-gradient(rgba(0,0,0,0.2),rgba(0,0,0,0.2)),center/cover_url(src/assets/motivation/Showreel.png)_no-repeat] flex justify-center items-center">
|
||||
<button className="p-[22px] rounded-full border bg-[#14161F33]">
|
||||
<FullScreenIcon />
|
||||
</button>
|
||||
|
||||
@@ -38,8 +38,11 @@ const Stand = forwardRef<HTMLDivElement, IStand>(
|
||||
<div className="flex justify-between">
|
||||
<div className="space-y-2">
|
||||
<p className="h4 font-medium">{title}</p>
|
||||
<div className="flex gap-x-2 items-center">
|
||||
<div className="w-2 h-2 bg-[#737AA1]" />
|
||||
<p className="text-xs text-[#737AA1] font-medium">{annotation}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="h4 font-medium">{year}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export function BurgerIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M19 8H5V6H17L19 8Z" fill="#9299BD" />
|
||||
<path d="M5 11H19V13H5V11Z" fill="#9299BD" />
|
||||
<path d="M5 16H19V18H7L5 16Z" fill="#9299BD" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { ReactNode, useEffect, useReducer, useRef, useState } from 'react';
|
||||
import { useSwipeable } from 'react-swipeable';
|
||||
import { SliderControls } from './SliderControls';
|
||||
|
||||
export function SliderWithScaling<T extends { title: string }>({
|
||||
export function SliderWithScaling<T extends { img: string }>({
|
||||
slides,
|
||||
SlideElement,
|
||||
className = '',
|
||||
@@ -114,7 +114,7 @@ export function SliderWithScaling<T extends { title: string }>({
|
||||
{order.map((slide, index) => (
|
||||
<motion.div
|
||||
key={
|
||||
slide.title +
|
||||
slide.img +
|
||||
(index < 1 ? '123' : index > slides.length ? '456' : '')
|
||||
}
|
||||
initial={
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export const hashes = new Map<number, string>([
|
||||
[1, 'interactive'],
|
||||
[4, '3d_makets'],
|
||||
[5, 'vr_ar'],
|
||||
]);
|
||||
@@ -1,3 +0,0 @@
|
||||
import { PhoneCode } from '../types/PhoneCode';
|
||||
|
||||
export const phoneCodes: PhoneCode[] = ['+7', '+375', '+380', '+44'];
|
||||
@@ -2,21 +2,63 @@ import { IStand } from '../types/IStand';
|
||||
|
||||
export const stands: IStand[] = [
|
||||
{
|
||||
img: 'src/assets/stands/1.png',
|
||||
title: 'Стенд 1',
|
||||
annotation: 'Презентация',
|
||||
img: 'src/assets/stands/graff_estate.png',
|
||||
title: 'GRAFF.estate',
|
||||
annotation: 'Движение',
|
||||
year: '2024',
|
||||
},
|
||||
{
|
||||
img: 'src/assets/stands/2.png',
|
||||
title: 'Стенд 2',
|
||||
annotation: 'ГК Основа, Москва',
|
||||
img: 'src/assets/stands/graff_estate2.png',
|
||||
title: 'GRAFF.estate',
|
||||
annotation: 'Форум «ДСФ “Диалоги-2024”»',
|
||||
year: '2024',
|
||||
},
|
||||
{
|
||||
img: 'src/assets/stands/3.png',
|
||||
title: 'Стенд 3',
|
||||
annotation: 'ГК Паритет Девелопмент, Тюмень',
|
||||
year: '2020',
|
||||
img: 'src/assets/stands/nks_development.png',
|
||||
title: 'НКС-девелопмент',
|
||||
annotation: 'TechnoBuild',
|
||||
year: '2023',
|
||||
},
|
||||
{
|
||||
img: 'src/assets/stands/nks_development2.png',
|
||||
title: 'НКС-девелопмент',
|
||||
annotation: 'TechnoBuild',
|
||||
year: '2023',
|
||||
},
|
||||
{
|
||||
img: 'src/assets/stands/interactive_floor.png',
|
||||
title: 'Interactive floor',
|
||||
annotation: 'Выставка «Россия ВДНХ»',
|
||||
year: '2022',
|
||||
},
|
||||
{
|
||||
img: 'src/assets/stands/graff_estate_stream.png',
|
||||
title: 'Graff.estate stream',
|
||||
annotation: 'WOW FEST',
|
||||
year: '2023',
|
||||
},
|
||||
{
|
||||
img: 'src/assets/stands/vr_training.png',
|
||||
title: 'VR-тренажер',
|
||||
annotation: 'Иннопром',
|
||||
year: '2019',
|
||||
},
|
||||
{
|
||||
img: 'src/assets/stands/graff_estate3.png',
|
||||
title: 'GRAFF.estate',
|
||||
annotation: 'Иннопром',
|
||||
year: '2019',
|
||||
},
|
||||
{
|
||||
img: 'src/assets/stands/ugmk-telecom.png',
|
||||
title: 'УГМК-Телеком',
|
||||
annotation: 'Иннопром',
|
||||
year: '2019',
|
||||
},
|
||||
{
|
||||
img: 'src/assets/stands/interactive_presentation.png',
|
||||
title: 'Интерактивная презентация',
|
||||
annotation: 'Армия 2019',
|
||||
year: '2019',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Devices } from '../components/Devices';
|
||||
|
||||
export function MainPage() {
|
||||
return (
|
||||
<div className="space-y-[180px]">
|
||||
<div className="lg:space-y-[180px] sm:space-y-[140px]">
|
||||
<Motivation />
|
||||
<Promotion />
|
||||
<Projects />
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
|
||||
export type Lang = 'RU' | 'EN';
|
||||
|
||||
export const useLanguageStore = create<{
|
||||
lang: Lang;
|
||||
setLang: (lang: Lang) => void;
|
||||
}>()(
|
||||
devtools(
|
||||
persist(
|
||||
set => ({
|
||||
lang: 'RU',
|
||||
setLang: (lang: Lang) => {
|
||||
set({ lang: lang });
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'lang',
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -893,6 +893,11 @@ convert-source-map@^2.0.0:
|
||||
resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz"
|
||||
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"
|
||||
@@ -1295,11 +1300,6 @@ input-format@^0.3.10:
|
||||
dependencies:
|
||||
prop-types "^15.8.1"
|
||||
|
||||
intl-tel-input@^24.3.6:
|
||||
version "24.3.6"
|
||||
resolved "https://registry.yarnpkg.com/intl-tel-input/-/intl-tel-input-24.3.6.tgz#e1deb7158888479041d72a1ab442730b9c59abc7"
|
||||
integrity sha512-27kLHadPt9dSAqc5zR0VHxW2zOBThQ8ctrouVNcm3s+0otyKA3dsQFyuYRnlTV5Kux/PPv7RejQf1Fw32xwUcA==
|
||||
|
||||
invariant@^2.2.4:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
||||
@@ -1781,6 +1781,11 @@ 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.npmjs.org/react/-/react-18.3.1.tgz"
|
||||
|
||||