diff --git a/public/img/pages/web/faq/faq_0.png b/public/img/pages/web/faq/faq_0.png index 3ebda1fa..c5d2104c 100644 Binary files a/public/img/pages/web/faq/faq_0.png and b/public/img/pages/web/faq/faq_0.png differ diff --git a/public/img/pages/web/faq/faq_2.png b/public/img/pages/web/faq/faq_2.png index ca7773a8..9ef206cc 100644 Binary files a/public/img/pages/web/faq/faq_2.png and b/public/img/pages/web/faq/faq_2.png differ diff --git a/src/components/modals/MapPointFormModal.tsx b/src/components/modals/MapPointFormModal.tsx index babab2cb..5475da69 100644 --- a/src/components/modals/MapPointFormModal.tsx +++ b/src/components/modals/MapPointFormModal.tsx @@ -85,15 +85,28 @@ export function MapPointProjectFormModal({ className="px-8 py-7 rounded-2xl outline-none bg-[#37393B99] bg-no-repeat bg-[right_32px_top_24px] h-fit font-medium btnl appearance-none" > - {companies?.map((company) => ( - - ))} + {companies + ?.sort((a, b) => { + // Проверяем, содержит ли название русские символы + const aHasCyrillic = /[а-яё]/i.test(a.title); + const bHasCyrillic = /[а-яё]/i.test(b.title); + + // Если одно название латиница, а другое кириллица + if (!aHasCyrillic && bHasCyrillic) return -1; // латиница идет первой + if (aHasCyrillic && !bHasCyrillic) return 1; // кириллица идет второй + + // Если оба одного типа, сортируем алфавитно + return a.title.localeCompare(b.title); + }) + .map((company) => ( + + ))} } diff --git a/src/components/modals/ProjectFormModal.tsx b/src/components/modals/ProjectFormModal.tsx index 930cc10a..45532f48 100644 --- a/src/components/modals/ProjectFormModal.tsx +++ b/src/components/modals/ProjectFormModal.tsx @@ -130,15 +130,28 @@ export function ProjectFormModal({ className="px-8 py-7 rounded-2xl outline-none bg-[#37393B99] bg-no-repeat bg-[right_32px_top_24px] h-fit font-medium btnl appearance-none" > - {companies?.map((company) => ( - - ))} + {companies + ?.sort((a, b) => { + // Проверяем, содержит ли название русские символы + const aHasCyrillic = /[а-яё]/i.test(a.title); + const bHasCyrillic = /[а-яё]/i.test(b.title); + + // Если одно название латиница, а другое кириллица + if (!aHasCyrillic && bHasCyrillic) return -1; // латиница идет первой + if (aHasCyrillic && !bHasCyrillic) return 1; // кириллица идет второй + + // Если оба одного типа, сортируем алфавитно + return a.title.localeCompare(b.title); + }) + .map((company) => ( + + ))} } diff --git a/src/components/modals/QuestionFormModal.tsx b/src/components/modals/QuestionFormModal.tsx index 7840813d..9b9570b7 100644 --- a/src/components/modals/QuestionFormModal.tsx +++ b/src/components/modals/QuestionFormModal.tsx @@ -1,86 +1,241 @@ -import React, { useRef } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import { Button } from "@/ui/Button"; import QuestionFormInput from "@/ui/QuestionFormInput"; import { useOnClickOutside } from "usehooks-ts"; import { useModalStore } from "@/stores/useModalStore"; +import { FormProvider, useForm } from "react-hook-form"; +import CheckIcon from "../icons/CheckIcon"; +import ReactInputMask from "react-input-mask"; +import { projectsTags } from "@/consts/projectsTags"; +import { CheckboxesGroup } from "@/ui/CheckboxesGroup"; +import { Product } from "@/types/Product"; +import FeedbackModal from "./FeedbackFormModal"; +import { api } from "@/api"; +import { useRefererStore } from "@/stores/useRefererStore"; +import { Country } from "react-phone-number-input"; +import { getExampleNumber } from "libphonenumber-js"; +import examples from "libphonenumber-js/mobile/examples"; + +interface IInput { + fullname: string; + phone: string; + email: string; + products: Product[]; + referer?: string | null; +} export default function QuestionFormModal() { - const { setModal } = useModalStore(); + const [[phoneCode, country], setPhoneCodeAndCountry] = useState< + [string, Country] + >(["+7", "RU"]); + + const { referer } = useRefererStore(); + const { modal, setModal } = useModalStore(); + + const placeholder = useMemo( + () => + getExampleNumber(country, examples) + ?.formatInternational() + .split(" ") + .slice(1) + .join(" "), + [country] + ); + const formRef = useRef(null); useOnClickOutside(formRef, () => { setModal(null); }); + const form = useForm({ + defaultValues: { + products: ["Создание сайтов", "Web-тур по 360 сферам"] as Product[], + }, + }); + + const { register, handleSubmit, formState, setValue } = form; + + async function onSubmit(data: IInput) { + const { id } = await api + .post("mail", { json: { ...data, referer } }) + .json<{ id: string }>(); + setModal(); + } + return ( -
-

- - {" "} - Напишите свой вопрос,{" "} - -
мы свяжемся с вами
в - ближайшее время -
и ответим на него -

- -
- - -
- - - -
- - - *Нажимая кнопку отправить, вы принимаете  - + - условия использования{" "} - -
 и  - - политику конфиденциальности - -
-
- +
+

Нам нужно

+ +
+ + +
+ { + if (e.nativeEvent.type.startsWith("input")) { + const cleanValue = e.target.value.replaceAll(/ /g, ""); + const inputType = (e.nativeEvent as InputEvent)?.inputType; + + const shouldAddPhoneCode = + inputType !== "insertFromPaste" && + inputType !== "insertFromDrop" && + inputType !== "insertCompositionText" && + !cleanValue.startsWith("+") && + !cleanValue.startsWith("7") && + !cleanValue.startsWith(phoneCode.replace("+", "")); + + form.setValue( + "phone", + (shouldAddPhoneCode ? phoneCode : "") + cleanValue + ); + } + }} + id={"tel"} + maskChar={null} + mask={"+7 " + (placeholder?.replace(/\d/g, "9") ?? "")} + placeholder={"+7 " + placeholder} + className="placeholder:btnl placeholder:font-medium placeholder:select-none peer btnl w-full h-full bg-transparent rounded-none transition-all outline-none" + /> +
+
+
+ +
+ + *Нажимая кнопку отправить, вы даете + {" "} + + согласие на обработку персональных данных + {" "} + и принимаете  + + условия политики + +
+
+ + + ) : ( +
+
+
+
+ +
+
+

+ Мы получили заявку +
и скоро свяжемся с вами! +

+
+
+ )} +
+ //
+ //

+ // + // {" "} + // Напишите свой вопрос,{" "} + // + //
мы свяжемся с вами
в + // ближайшее время + //
и ответим на него + //

+ + //
+ // + // + //
+ + // + + //
+ // + // + // *Нажимая кнопку отправить, вы принимаете + // + // условия использования{" "} + // + //
 и + // + // политику конфиденциальности + // + //
+ //
+ // ); } diff --git a/src/components/pages/AboutPage/AboutEventCard.tsx b/src/components/pages/AboutPage/AboutEventCard.tsx index 0d073403..547e4bce 100644 --- a/src/components/pages/AboutPage/AboutEventCard.tsx +++ b/src/components/pages/AboutPage/AboutEventCard.tsx @@ -46,14 +46,14 @@ export default function AboutEventCard({ style={{ backgroundColor: color }} className="size-2.5 rounded-full mr-2" /> - {location} + {location}
- + {dates .map((date) => format(date, "d MMMM", { locale: ru })) .join(" – ")} diff --git a/src/components/pages/AboutPage/AboutEvents.tsx b/src/components/pages/AboutPage/AboutEvents.tsx index 788e33ad..224b8838 100644 --- a/src/components/pages/AboutPage/AboutEvents.tsx +++ b/src/components/pages/AboutPage/AboutEvents.tsx @@ -19,7 +19,7 @@ function AboutEvents() { где каждый девелопер может лично протестировать{" "}
- и оценить функционал наших интерактивных макетов недвижимости + и оценить функционал наших интерактивных инструментов продаж

}> diff --git a/src/components/pages/AboutPage/AboutExperience.tsx b/src/components/pages/AboutPage/AboutExperience.tsx index dfbd7898..1aba8c05 100644 --- a/src/components/pages/AboutPage/AboutExperience.tsx +++ b/src/components/pages/AboutPage/AboutExperience.tsx @@ -48,7 +48,7 @@ export default function AboutExperience() { > - Виртуальные туры
по 360 сферам + Виртуальные туры
по 360° сферам
@@ -153,9 +153,9 @@ export default function AboutExperience() { в 40 выставках

- Наш продукт GRAFF.estate уже четыре раза был представлен - на форуме “Движение”, Иннопром, Восточный экономический форум, - 100+ TechnoBuild + Продукты GRAFF.estate уже четыре раза были представлены + на форуме «Движение», Восточном экономическом форуме, Иннопроме + и форуме 100+ TechnoBuild

Заняли 1 место на WOW AWARDS 2024 совместно с застройщиком Upside Development -

- Мы заняли 1 место на WOW AWARDS 2024 совместно c застройщиком - Upside Development +

+ Мы дважды заняли 1 место на WOW AWARDS в 2024 и 2025 году + совместно с застройщиками Upside Development и LEGENDA

diff --git a/src/components/pages/WebPage/PageComponents/Accordeon/Accordeon.tsx b/src/components/pages/WebPage/PageComponents/Accordeon/Accordeon.tsx index ade9160a..8ee7d830 100644 --- a/src/components/pages/WebPage/PageComponents/Accordeon/Accordeon.tsx +++ b/src/components/pages/WebPage/PageComponents/Accordeon/Accordeon.tsx @@ -159,7 +159,6 @@ export default function Accordeon({ ease: "easeIn", }, }} - className="text1 lg:text-[0.972vw] md:text-[1.823vw] text-[3.889vw]" key={index} > {item.content} diff --git a/src/components/pages/WebPage/PageComponents/WebInfiniteSlider/ContentSlideIphone.tsx b/src/components/pages/WebPage/PageComponents/WebInfiniteSlider/ContentSlideIphone.tsx index bd517d5f..89575a1e 100644 --- a/src/components/pages/WebPage/PageComponents/WebInfiniteSlider/ContentSlideIphone.tsx +++ b/src/components/pages/WebPage/PageComponents/WebInfiniteSlider/ContentSlideIphone.tsx @@ -7,35 +7,35 @@ function Iphone({ active }: { active: boolean }) { return (
{active ? ( ) : ( )} diff --git a/src/components/pages/WebPage/WebDemo.tsx b/src/components/pages/WebPage/WebDemo.tsx index b9aebc8b..5bf13f4b 100644 --- a/src/components/pages/WebPage/WebDemo.tsx +++ b/src/components/pages/WebPage/WebDemo.tsx @@ -106,7 +106,7 @@ export default function WebDemo() {

- Люди гуляют в окрестностях ЖК + Люди гуляют в окрестностях

@@ -180,7 +180,7 @@ export default function WebDemo() {

- Фасады ЖК можно изучить со всех сторон + Фасады можно изучить со всех сторон

diff --git a/src/components/pages/WebPage/data/TimeLineData.ts b/src/components/pages/WebPage/data/TimeLineData.ts index b9121974..f9389601 100644 --- a/src/components/pages/WebPage/data/TimeLineData.ts +++ b/src/components/pages/WebPage/data/TimeLineData.ts @@ -1,6 +1,6 @@ import { IAccordeonContent } from "@/types/IAccordeon"; -export const WebTimelineData: { [key: string]: IAccordeonContent[] } = { +export const WebTimelineData: { [key: string]: IAccordeonContent[] } = { "Сбор данных и анализ": [ { type: "general", @@ -27,8 +27,7 @@ export const WebTimelineData: { [key: string]: IAccordeonContent[] } = { }, { type: "new", - content: - "Получаем исходные данные: чертежи, фотографии, сканы помещений", + content: "Получаем исходные данные: чертежи, фотографии, сканы помещений", }, { type: "general", @@ -36,22 +35,25 @@ export const WebTimelineData: { [key: string]: IAccordeonContent[] } = { }, ], - "Прототипирование": [ + Прототипирование: [ { type: "new", content: "Моделируем пространства в Blender и 3Ds Max", }, { type: "general", - content: "Оптимизируем полигоны для использования на сайте: уменьшаем нагрузку", + content: + "Оптимизируем полигоны для использования на сайте: уменьшаем нагрузку", }, { type: "new", - content: "Создаем материалы и текстуры для реализма картинки с помощью PBR-рендеринга ", + content: + "Создаем материалы и текстуры для реализма картинки с помощью PBR-рендеринга ", }, { type: "general", - content: "Создаем предварительный рендер и согласовываем общий стиль проекта", + content: + "Создаем предварительный рендер и согласовываем общий стиль проекта", }, ], @@ -68,10 +70,6 @@ export const WebTimelineData: { [key: string]: IAccordeonContent[] } = { type: "general", content: "Отрисовываем дизайн-макеты ключевых блоков", }, - { - type: "general", - content: "Согласование с заказчиком и правки", - }, ], "Дизайн сайта": [ @@ -91,16 +89,13 @@ export const WebTimelineData: { [key: string]: IAccordeonContent[] } = { type: "general", content: "Наполняем страницы контентом", }, - { - type: "general", - content: "Согласование с заказчиком и правки", - }, ], "Сборка сцен": [ { type: "general", - content: "Компонуем виртуальные сцены: расставляем мебель, освещение и камеры", + content: + "Компонуем виртуальные сцены: расставляем мебель, освещение и камеры", }, { type: "general", @@ -108,7 +103,7 @@ export const WebTimelineData: { [key: string]: IAccordeonContent[] } = { "Готовим тестовые интерактивные 360° панорамы и экспортируем в веб-формат", }, ], - "Рендеринг": [ + Рендеринг: [ { type: "general", content: "Готовим финальные версии всех интерактивных 360° панорам ", @@ -118,7 +113,7 @@ export const WebTimelineData: { [key: string]: IAccordeonContent[] } = { content: "Экспортируем готовые панорамы в на сайт", }, ], - "Верстка сайта": [ + "Верстка сайта": [ { type: "general", content: "Верстаем (HTML/CSS/JS) по макетам  ", @@ -126,13 +121,14 @@ export const WebTimelineData: { [key: string]: IAccordeonContent[] } = { { type: "general", content: "Настраиваем анимации и интерактив на сайте", - }, + }, { type: "general", - content: "Адаптируем страницы под мобильные устройства: жесты, touch-управление", + content: + "Адаптируем страницы под мобильные устройства: жесты, touch-управление", }, ], - "Оптимизация": [ + Оптимизация: [ { type: "general", content: "Тестируем готовый сайт на разных устройствах.", diff --git a/src/lib/LenisProvider.tsx b/src/lib/LenisProvider.tsx index 170d2f94..6f7217bc 100644 --- a/src/lib/LenisProvider.tsx +++ b/src/lib/LenisProvider.tsx @@ -10,8 +10,11 @@ import { useState, } from "react"; import { useModalStore } from "@/stores/useModalStore"; +import { useAuthStore } from "@/stores/useAuthStore"; +import { useCheckAuthQuery } from "@/queries/checkAuth"; export function LenisProvider({ children }: PropsWithChildren) { + const { data: auth } = useCheckAuthQuery(); const lenis = useRef(null); const { modal } = useModalStore(); const [lenisKey, setLenisKey] = useState(0); @@ -35,6 +38,7 @@ export function LenisProvider({ children }: PropsWithChildren) { ); useEffect(() => { + if (!auth) return; if (modal) { // Отключаем Lenis и блокируем скролл body lenis.current?.lenis?.destroy(); @@ -61,7 +65,7 @@ export function LenisProvider({ children }: PropsWithChildren) { lenis.current?.lenis?.start(); document.body.style.overflow = ""; } - }, [modal]); + }, [modal, auth]); return ( api.get('auth/check').json<{ auth: boolean }>(), + queryKey: ["checkAuth"], + queryFn: () => api.get("auth/check").json<{ auth: boolean }>(), select: (data) => data.auth, enabled: !!token, });