This commit is contained in:
2026-04-10 17:16:13 +05:00
commit f4b2b9b67e
80 changed files with 5776 additions and 0 deletions
+167
View File
@@ -0,0 +1,167 @@
import { api } from "@/lib/api";
import { projectsTags } from "@/consts/projectsTags";
import type { Product } from "@/types/Product";
import { Button } from "@/ui/Button";
import { CheckboxesGroup } from "@/ui/CheckboxesGroup";
import { PhoneInputRu } from "@/ui/PhoneInputRu";
import useAddReferer from "@/hooks/useAddReferer";
import { useRefererStore } from "@/stores/useRefererStore";
import { useModalStore } from "@/stores/useModalStore";
import {
Controller,
FormProvider,
useForm,
} from "react-hook-form";
import CheckIcon from "@/components/icons/CheckIcon";
import FeedbackModal from "@/components/modals/FeedbackFormModal";
const DEFAULT_STREAM_DEMO_PRODUCTS = [
"Удаленная демонстрация",
] as Product[];
export function Feedback() {
useAddReferer();
return (
<div
id="contacts"
className="lg:mb-20 md:mb-12 lg:flex lg:gap-[0.833vw] max-lg:space-y-12 justify-between lg:mt-[14.07vh] mt-[100px] mb-10"
>
<h2 className="line2 font-medium max-lg:mb-6 lg:max-w-[45%]">
<span className="text-[#7A7A7A]">Хотите увеличить конверсию?</span>
<br />
Давайте обсудим детали.
</h2>
<FeedbackForm />
</div>
);
}
interface IInput {
fullname: string;
phone: string;
email: string;
products: Product[];
referer?: string | null;
}
export function FeedbackForm() {
const { referer } = useRefererStore();
const { setModal } = useModalStore();
const form = useForm<IInput>({
defaultValues: {
fullname: "",
email: "",
phone: "",
products: DEFAULT_STREAM_DEMO_PRODUCTS,
},
});
const { register, handleSubmit, formState, control } = form;
async function onSubmit(data: IInput) {
const { id } = await api
.post("mail", { json: { ...data, referer } })
.json<{ id: string }>();
setModal(<FeedbackModal id={id} />);
}
return (
<div className="flex-1 space-y-4 lg:max-w-[47.431vw]">
<div className="space-y-10">
{!formState.isSubmitted ? (
<FormProvider {...form}>
<form
className="lg:space-y-[1.944vw] md:max-lg:space-y-7 space-y-3"
onSubmit={handleSubmit(onSubmit)}
>
<div className="lg:space-y-[1.111vw] space-y-4">
<p className="heading2 font-medium">Нам нужно</p>
<CheckboxesGroup name="products" options={projectsTags} />
</div>
<input
id="name"
autoComplete="none"
type="text"
required
placeholder="Имя*"
{...register("fullname")}
className="bg-transparent border-b border-[#37393B] focus:border-white py-4 rounded-none outline-none transition-all w-full placeholder:btnl btnl placeholder:font-medium placeholder:select-none"
/>
<input
autoComplete="none"
required
id="email"
type="email"
placeholder="Email*"
{...register("email")}
className="bg-transparent border-b border-[#37393B] focus:border-white py-4 rounded-none btnl outline-none transition-all w-full placeholder:btnl placeholder:font-medium placeholder:select-none"
/>
<div className="flex gap-x-3 py-2 border-[#3D425C] relative">
<Controller
name="phone"
control={control}
rules={{ required: true }}
render={({ field }) => (
<PhoneInputRu
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
inputRef={field.ref}
/>
)}
/>
<div className="bottom-0 absolute w-full border-b border-[#37393B] peer-focus:border-white -mb-2" />
</div>
<div className="md:flex items-stretch lg:gap-[0.833vw] gap-3">
<Button
type="submit"
className="btnl max-md:mb-3 max-md:w-full lg:px-[2.222vw] lg:py-[1.389vw] px-8 py-5 cursor-pointer lg:rounded-[1.111vw] rounded-2xl"
>
Оставить заявку
</Button>
<div className="text2 xl:max-w-[60%] md:max-lg:max-w-[40%] md:max-lg:py-1">
<span className="text-[#7A7A7A]">
*Нажимая кнопку отправить, вы даете
</span>{" "}
<a
target="_blank"
rel="noopener noreferrer"
href={"/privacy-policy"}
className="underline"
>
согласие на обработку персональных данных
</a>{" "}
<span className="text-[#7A7A7A]">и принимаете </span>
<a
target="_blank"
rel="noopener noreferrer"
href={"/policy"}
className="underline"
>
условия политики
</a>
</div>
</div>
</form>
</FormProvider>
) : (
<div className="bg-[#37393B99] aspect-[643/480] w-full rounded-2xl flex justify-center items-center">
<div className="flex gap-3 justify-center items-center">
<div className="bg-gradient p-3 rounded-full">
<div className="text-white lg:size-[1.667vw] size-6">
<CheckIcon />
</div>
</div>
<p className="heading2 font-medium">
Мы получили заявку
<br />и скоро свяжемся с вами!
</p>
</div>
</div>
)}
</div>
</div>
);
}
+130
View File
@@ -0,0 +1,130 @@
import { PropsWithChildren } from "react";
import ArrowMoreIcon from "@/components/icons/ArrowMoreIcon";
import RutubeIcon from "@/components/icons/RutubeIcon";
import TelegramIcon from "@/components/icons/TgIcon";
import VkIcon from "@/components/icons/VKIcon";
import YoutubeIcon from "@/components/icons/YoutubeIcon";
export function Footer() {
return (
<footer className="lg:px-5 lg:pb-5 md:max-lg:px-4 md:max-lg:pb-4 px-[10px] pb-[10px] space-y-6 mb-0">
<div className="max-md:flex-col lg:gap-[0.833vw] md:max-lg:gap-[1.042vw] flex gap-[1.111vw]">
<a
href={"tel:" + "8 800 770 00 67".replaceAll(" ", "")}
className="lg:p-[1.667vw] p-6 flex flex-col justify-between max-md:gap-y-10 bg-[linear-gradient(to_top_left,#7A7A7A50,transparent)] lg:rounded-[1.111vw] rounded-2xl lg:aspect-[696/248] lg:w-[48.333vw] md:max-lg:w-[47.656vw] hover:bg-[#37393B99] bg-transparent transition-colors"
>
<div className="text-[#7A7A7A] text1 font-medium">Позвонить</div>
<div className="lg:line2 md:max-lg:heading1 line2 flex items-center font-medium">
8 800 770 00 67
<div className="text-white lg:size-[5.556vw] size-[10vw]">
<ArrowMoreIcon />
</div>
</div>
</a>
<a
href={"mailto:" + "info@graff.tech"}
className="lg:p-[1.667vw] p-6 flex flex-col justify-between max-md:gap-y-10 bg-[linear-gradient(to_top_left,#7A7A7A50,transparent)] lg:rounded-[1.111vw] rounded-2xl lg:aspect-[624/248] lg:w-[43.333vw] md:max-lg:w-[47.656vw] hover:bg-[#37393B99] bg-transparent transition-colors"
>
<div className="text-[#7A7A7A] text1 font-medium">Написать</div>
<div className="lg:line2 md:max-lg:heading1 line2 flex items-center font-medium">
info@graff.tech
<div className="text-white lg:size-[5.556vw] size-[10vw]">
<ArrowMoreIcon />
</div>
</div>
</a>
<div className="gap-y-2 justify-between md:gap-x-[1.042vw] gap-x-[1.111vw] md:flex-col flex">
<ContactLink href="https://t.me/graffestate">
<div className="text-white lg:size-[1.389vw] size-[5.556vw] group-hover:text-black">
<TelegramIcon />
</div>
</ContactLink>
<ContactLink href="https://rutube.ru/channel/25505040">
<div className="text-white lg:size-[1.389vw] size-[5.556vw] group-hover:text-black">
<RutubeIcon />
</div>
</ContactLink>
<ContactLink href="https://vk.com/graff.estate">
<div className="text-white lg:size-[1.389vw] size-[5.556vw] group-hover:text-black">
<VkIcon />
</div>
</ContactLink>
<ContactLink href="https://www.youtube.com/@GRAFFtech">
<div className="text-white lg:size-[1.389vw] size-[5.556vw] group-hover:text-black">
<YoutubeIcon />
</div>
</ContactLink>
</div>
</div>
<div className="lg:w-full flex flex-col md:flex-row md:max-lg:gap-[1.042vw] lg:gap-x-[0.833vw] gap-6 lg:pb-6 max-lg:py-6 pb-10 md:max-lg:pt-4 !max-md:mt-[11.111vw] border-b border-[#232425] relative">
<div className="flex flex-col gap-y-[1.111vw] lg:min-w-[48.193vw] flex-1">
<span className=" text1 text-[#7A7A7A]">Юридический адрес:</span>
<span className="headline2 max-md:leading-4 font-medium text-[#7A7A7A]">
620063, г. Екатеринбург, <br /> ул. Большакова, д. 66, кв. 6
</span>
<div className="flex flex-col gap-y-[1.111vw] lg:mt-[0px] md:mt-[2.083vw] mt-6">
<span className=" text1 text-[#7A7A7A]">Наш основной стек:</span>
<div className="headline2 max-md:leading-4 font-medium text-[#7A7A7A]">
<p>Unreal Engine 5, C++</p>
</div>
</div>
</div>
<div className="flex flex-col gap-y-[1.111vw] flex-1">
<span className="text1 text-[#7A7A7A]">Реквизиты:</span>
<div className="headline2 max-md:leading-4 font-medium text-[#7A7A7A]">
<p>ИНН: 6679174128</p>
<p>КПП: 667101001</p>
<p>ООО &quot;ГРАФФ.ЭСТЕЙТ&quot;</p>
<p>ОГРН 1246600010140</p>
</div>
</div>
<img
src="/img/components/header/Sk.svg"
alt="Сколково"
className=" lg:hidden md:size-[6.25vw] size-[13.333vw] max-md:absolute max-md:right-0 max-md:bottom-6"
/>
</div>
<div className="lg:gap-x-[0.833vw] gap-y-2 flex max-lg:flex-col">
<a
target="_blank"
rel="noopener noreferrer"
href={"/policy"}
className="text-[#37393B] text1 font-medium leading-[18.9px] lg:w-[48.193vw] w-fit"
>
Политика конфиденциальности и обработки персональных данных
</a>
<p className="text-[#37393B] text1 font-medium leading-[18.9px] col-start-1">
© 2026 GRAFF interactive. Все права защищены
</p>
<a
target="_blank"
rel="noopener noreferrer"
href={"https://graff.tech"}
className="text-[#37393B] text1 font-medium leading-[18.9px] lg:ml-auto w-fit md:col-start-2 md:text-right"
>
graff.tech
</a>
</div>
</footer>
);
}
export function ContactLink({
children,
href,
className = "",
}: PropsWithChildren<{ href: string; className?: string }>) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className={`lg:rounded-[1.111vw] rounded-2xl bg-[#37393B99] lg:p-[1.25vw] p-[18px] hover:bg-white transition-all hover:text-black flex justify-center w-full group ${className}`}
>
{children}
</a>
);
}
+20
View File
@@ -0,0 +1,20 @@
import React from "react";
interface BRProps {
lg?: boolean;
md?: boolean;
sm?: boolean;
className?: string;
}
export default function BR({ lg, md, sm, className }: BRProps) {
const modifier =
!lg && !md && !sm
? ""
: `lg:${lg ? `block` : "hidden"} md:${md ? "block" : "hidden"} ${
sm ? "block" : "hidden"
} `;
const combinedClassName = `${modifier} ${className ?? ""}`;
return <br className={combinedClassName} />;
}
+27
View File
@@ -0,0 +1,27 @@
import { useModalStore } from "@/stores/useModalStore";
import { useEffect } from "react";
import { createPortal } from "react-dom";
export function ModalContainer() {
const { modal, setModal } = useModalStore();
useEffect(() => {
const listener = (e: KeyboardEvent) => {
if (e.key === "Escape") setModal(null);
};
document.addEventListener("keydown", listener);
return () => {
document.removeEventListener("keydown", listener);
};
}, [setModal]);
const jsx = modal ? (
<div className="fixed left-0 z-[20] w-full h-full flex justify-center items-start transition-opacity">
<div className="absolute [backdrop-filter:blur(16px)] bg-[#0F101199] w-full h-full z-[1]" />
{modal}
</div>
) : null;
return createPortal(jsx, document.body);
}