added clients marquee

This commit is contained in:
2024-09-02 16:23:03 +05:00
parent d7564182ed
commit 0bd4038e05
48 changed files with 391 additions and 10 deletions
+275
View File
@@ -0,0 +1,275 @@
import { api } from '../api';
import { phoneCodes } from '../consts/phoneCodes';
import { ClassNameWrapper } from '../hocs/ClassNameWrapper';
import { useModalStore } from '@/stores/useModalStore';
import { PhoneCode } from '@/types/PhoneCode';
import { Button } from '@/ui/Button';
import { FormEvent, useEffect, useRef, useState } from 'react';
import ReactInputMask from 'react-input-mask';
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';
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);
}
}
}
useEffect(() => {
const listener = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
setModal(false, 'form');
}
};
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 ? (
<div className="space-y-8">
<div className="flex justify-between items-center">
<p className="font-medium accent">Оставьте заявку</p>
<button
onClick={() => setModal(null, '')}
className="p-2 lg:hover:bg-white lg:hover:bg-opacity-10 transition-colors rounded-full"
>
<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>
</div>
) : (
<div className="">
<div className="space-y-8">
<h2 className="h2 font-medium">Спасибо за отправку заявки!</h2>
<p className="m-text">
Мы ценим ваш интерес к нашей компании и в ближайшее время свяжемся
с вами для уточнения деталей проекта.
</p>
</div>
<Button
width="full"
className="py-5 px-6 absolute top-[50vh]"
icon={
<ClassNameWrapper
className="w-6 h-6"
element={<ArrowRightIcon />}
/>
}
onClick={() => setModal(false, '')}
>
На главную
</Button>
</div>
)}
</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>
);
}
+2
View File
@@ -2,6 +2,7 @@ import { Outlet } from 'react-router-dom';
import { Footer } from './Footer';
import { Header } from './Header';
import { Feedback } from '../components/Feedback';
import { Clients } from '../components/Clients';
export function Layout() {
return (
@@ -10,6 +11,7 @@ export function Layout() {
<main className="lg:px-6">
<Outlet />
</main>
<Clients />
<Feedback />
<Footer />
</>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1021 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

+52
View File
@@ -0,0 +1,52 @@
import { clients } from '../consts/clients';
export function Clients() {
return (
<div className="space-y-6 overflow-hidden mt-16">
<div className="flex items-center overflow-hidden w-screen min-h-[117px]">
<MarqueeHalf items={clients.slice(0, clients.length / 3)} />
<MarqueeHalf items={clients.slice(0, clients.length / 3)} />
</div>
<div className="flex items-center overflow-hidden w-screen min-h-[117px]">
<MarqueeHalf
reversed
items={clients.slice(clients.length / 3, (2 * clients.length) / 3)}
/>
<MarqueeHalf
reversed
items={clients.slice(clients.length / 3, (2 * clients.length) / 3)}
/>
</div>
<div className="border-b border-[#3D425C] flex items-center overflow-hidden w-screen min-h-[117px] pb-16">
<MarqueeHalf items={clients.slice(2 * (clients.length / 3))} />
<MarqueeHalf items={clients.slice(2 * (clients.length / 3))} />
</div>
</div>
);
}
function MarqueeHalf({
reversed = false,
items,
}: {
items: { src: string }[];
reversed?: boolean;
}) {
return (
<div
className={
'flex flex-nowrap overflow-clip items-center py-2 [flex:0_0_auto] animate-infinite-scroll ' +
(reversed ? '[animation-direction:reverse]' : '')
}
>
{items.map(client => (
<img
key={client.src}
src={client.src}
alt={client.src}
className="max-w-full h-auto !relative object-covesr mx-12"
/>
))}
</div>
);
}
+1 -1
View File
@@ -4,7 +4,7 @@ import { Title } from './ui/Title';
export function Projects() {
return (
<div className="space-y-6 mt-[180px]">
<div id={'/projects'} className="space-y-6 mt-[180px]">
<Title>Проекты</Title>
<div className="flex flex-col gap-y-16">
<div className="flex gap-x-4">{projects.slice(0, 2).map(Project)}</div>
+1 -1
View File
@@ -11,7 +11,7 @@ export function TelegramIcon() {
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M5.44748 8H2.38424C1.50903 8 1.33398 8.39579 1.33398 8.83368C1.33398 9.61432 2.37198 13.4863 6.16953 18.6072C8.70064 22.1044 12.2671 24 15.5124 24C17.4598 24 17.7004 23.5789 17.7004 22.8539V20.2105C17.7004 19.3684 17.8842 19.2 18.5013 19.2C18.9564 19.2 19.7353 19.4189 21.5522 21.1048C23.6291 23.1032 23.9713 24 25.1397 24H28.203C29.0782 24 29.5158 23.5789 29.2629 22.7478C28.9872 21.92 27.9956 20.7183 26.6792 19.2943C25.9651 18.4825 24.8938 17.6076 24.57 17.1705C24.1149 16.6088 24.2462 16.3587 24.57 15.8594C24.57 15.8594 28.3019 10.8008 28.6922 9.08379C28.8865 8.45895 28.6922 8 27.7645 8H24.7021C23.9232 8 23.5644 8.39579 23.3701 8.83368C23.3701 8.83368 21.8122 12.4867 19.6058 14.8598C18.8916 15.5469 18.5669 15.7659 18.1774 15.7659C17.9831 15.7659 17.7013 15.5469 17.7013 14.9229V9.08379C17.7013 8.33432 17.4746 8 16.8261 8H12.0124C11.5258 8 11.2335 8.34779 11.2335 8.6779C11.2335 9.38779 12.3363 9.552 12.45 11.5495V15.8905C12.45 16.8421 12.2715 17.0147 11.8812 17.0147C10.8432 17.0147 8.3173 13.3448 6.81894 9.14611C6.52486 8.32842 6.22992 8 5.44748 8Z"
d="M4.35848 15.8486C10.7919 12.8128 15.0923 10.8506 17.2253 9.88797C23.3491 7.14829 24.622 6.66699 25.4477 6.66699C25.6197 6.66699 26.0325 6.70402 26.3077 6.92615C26.5142 7.11127 26.583 7.37042 26.6174 7.55554C26.6518 7.74065 26.6862 8.1479 26.6518 8.48111C26.3077 12.2204 24.8972 21.365 24.1403 25.5486C23.8307 27.3257 23.2114 27.9181 22.6266 27.9921C21.3537 28.1032 20.356 27.0665 19.1175 26.215C17.1909 24.8452 16.09 23.9936 14.1978 22.6608C12.0304 21.1059 13.4409 20.2543 14.6794 18.8845C14.9891 18.5143 20.6656 12.9979 20.7688 12.4796C20.7688 12.4055 20.8032 12.1834 20.6656 12.0723C20.528 11.9612 20.356 11.9983 20.2184 12.0353C20.0119 12.0723 16.9157 14.2937 10.8951 18.6624C10.0006 19.3288 9.20934 19.625 8.48687 19.625C7.69559 19.625 6.18185 19.1437 5.04655 18.7364C3.67042 18.2551 2.56952 17.996 2.67273 17.1815C2.77594 16.7372 3.32639 16.2929 4.35848 15.8486Z"
fill="white"
/>
</g>
+1 -1
View File
@@ -11,7 +11,7 @@ export function VKIcon() {
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M26.4181 7.23088C27.5654 7.54116 28.4689 8.45558 28.7756 9.6165C29.3327 11.7209 29.3327 16.1114 29.3327 16.1114C29.3327 16.1114 29.3327 20.502 28.7756 22.6063C28.4689 23.7671 27.5654 24.6816 26.4181 24.9918C24.3384 25.5559 15.9994 25.5559 15.9994 25.5559C15.9994 25.5559 7.66049 25.5559 5.58084 24.9918C4.43357 24.6816 3.5299 23.7671 3.22327 22.6063C2.66602 20.502 2.66602 16.1114 2.66602 16.1114C2.66602 16.1114 2.66602 11.7209 3.22327 9.6165C3.5299 8.45558 4.43357 7.54116 5.58084 7.23088C7.66049 6.66699 15.9994 6.66699 15.9994 6.66699C15.9994 6.66699 24.3384 6.66699 26.4181 7.23088ZM13.7771 12.2224V20.0002L20.4438 16.1114L13.7771 12.2224Z"
d="M5.44748 8H2.38424C1.50903 8 1.33398 8.39579 1.33398 8.83368C1.33398 9.61432 2.37198 13.4863 6.16953 18.6072C8.70064 22.1044 12.2671 24 15.5124 24C17.4598 24 17.7004 23.5789 17.7004 22.8539V20.2105C17.7004 19.3684 17.8842 19.2 18.5013 19.2C18.9564 19.2 19.7353 19.4189 21.5522 21.1048C23.6291 23.1032 23.9713 24 25.1397 24H28.203C29.0782 24 29.5158 23.5789 29.2629 22.7478C28.9872 21.92 27.9956 20.7183 26.6792 19.2943C25.9651 18.4825 24.8938 17.6076 24.57 17.1705C24.1149 16.6088 24.2462 16.3587 24.57 15.8594C24.57 15.8594 28.3019 10.8008 28.6922 9.08379C28.8865 8.45895 28.6922 8 27.7645 8H24.7021C23.9232 8 23.5644 8.39579 23.3701 8.83368C23.3701 8.83368 21.8122 12.4867 19.6058 14.8598C18.8916 15.5469 18.5669 15.7659 18.1774 15.7659C17.9831 15.7659 17.7013 15.5469 17.7013 14.9229V9.08379C17.7013 8.33432 17.4746 8 16.8261 8H12.0124C11.5258 8 11.2335 8.34779 11.2335 8.6779C11.2335 9.38779 12.3363 9.552 12.45 11.5495V15.8905C12.45 16.8421 12.2715 17.0147 11.8812 17.0147C10.8432 17.0147 8.3173 13.3448 6.81894 9.14611C6.52486 8.32842 6.22992 8 5.44748 8Z"
fill="white"
/>
</g>
+43
View File
@@ -0,0 +1,43 @@
import { IClient } from '../types/IClient';
export const clients: IClient[] = [
{ src: '/src/assets/clients/sezar_group.png' },
{ src: '/src/assets/clients/legenda.png' },
{ src: '/src/assets/clients/osnova.png' },
{ src: '/src/assets/clients/upside.png' },
{ src: '/src/assets/clients/brusnika.png' },
{ src: '/src/assets/clients/capital_group.png' },
{ src: '/src/assets/clients/a101.png' },
{ src: '/src/assets/clients/forum.png' },
{ src: '/src/assets/clients/leto.png' },
{ src: '/src/assets/clients/center.png' },
{ src: '/src/assets/clients/center-invest.png' },
{ src: '/src/assets/clients/rodina.png' },
{ src: '/src/assets/clients/acons.png' },
{ src: '/src/assets/clients/alfa.png' },
{ src: '/src/assets/clients/sk+.png' },
{ src: '/src/assets/clients/delom.png' },
{ src: '/src/assets/clients/kama.png' },
{ src: '/src/assets/clients/risan.png' },
{ src: '/src/assets/clients/golos.png' },
{ src: '/src/assets/clients/dns.png' },
{ src: '/src/assets/clients/sibintel.png' },
{ src: '/src/assets/clients/kortros.png' },
{ src: '/src/assets/clients/mayak.png' },
{ src: '/src/assets/clients/sbk.png' },
{ src: '/src/assets/clients/nks.png' },
{ src: '/src/assets/clients/atom.png' },
{ src: '/src/assets/clients/yit.png' },
{ src: '/src/assets/clients/sinara.png' },
{ src: '/src/assets/clients/pik.png' },
{ src: '/src/assets/clients/as.png' },
{ src: '/src/assets/clients/efes.png' },
{ src: '/src/assets/clients/atmosfera.png' },
{ src: '/src/assets/clients/abudhabi.png' },
{ src: '/src/assets/clients/mavis.png' },
{ src: '/src/assets/clients/enko.png' },
{ src: '/src/assets/clients/paritet.png' },
{ src: '/src/assets/clients/fortis.png' },
{ src: '/src/assets/clients/ugmk.png' },
{ src: '/src/assets/clients/atlas.png' },
];
+3
View File
@@ -0,0 +1,3 @@
export interface IClient {
src: string;
}
+13 -7
View File
@@ -1,12 +1,18 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {},
extend: {
animation: {
'infinite-scroll': 'infinite-scroll 25s linear infinite',
},
keyframes: {
'infinite-scroll': {
from: { transform: 'translateX(0)' },
to: { transform: 'translateX(-100%)' },
},
},
},
},
plugins: [],
}
};