diff --git a/src/assets/emoji/1.png b/src/assets/emoji/1.png new file mode 100644 index 0000000..bcd7daa Binary files /dev/null and b/src/assets/emoji/1.png differ diff --git a/src/assets/emoji/2.png b/src/assets/emoji/2.png new file mode 100644 index 0000000..87bc784 Binary files /dev/null and b/src/assets/emoji/2.png differ diff --git a/src/assets/emoji/3.png b/src/assets/emoji/3.png new file mode 100644 index 0000000..5c74938 Binary files /dev/null and b/src/assets/emoji/3.png differ diff --git a/src/assets/emoji/4.png b/src/assets/emoji/4.png new file mode 100644 index 0000000..58c8f63 Binary files /dev/null and b/src/assets/emoji/4.png differ diff --git a/src/assets/emoji/5.png b/src/assets/emoji/5.png new file mode 100644 index 0000000..79969c8 Binary files /dev/null and b/src/assets/emoji/5.png differ diff --git a/src/assets/emoji/china.png b/src/assets/emoji/china.png new file mode 100644 index 0000000..2412212 Binary files /dev/null and b/src/assets/emoji/china.png differ diff --git a/src/assets/emoji/korea.png b/src/assets/emoji/korea.png new file mode 100644 index 0000000..81c69f4 Binary files /dev/null and b/src/assets/emoji/korea.png differ diff --git a/src/assets/emoji/united-states.png b/src/assets/emoji/united-states.png new file mode 100644 index 0000000..abe1c5f Binary files /dev/null and b/src/assets/emoji/united-states.png differ diff --git a/src/components/Feedback.tsx b/src/components/Feedback.tsx index b883f9b..c0c665f 100644 --- a/src/components/Feedback.tsx +++ b/src/components/Feedback.tsx @@ -12,6 +12,8 @@ import { InputLabel, OutlinedInput, Checkbox, + Dialog, + DialogContent, } from "@mui/material"; import CloseIcon from "@mui/icons-material/Close"; import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; @@ -53,7 +55,11 @@ interface FeedbackProps { handleScrollToAnchor?: (anchor: string) => void; } -const Feedback: React.FC = ({ open, onClose, handleScrollToAnchor }) => { +const Feedback: React.FC = ({ + open, + onClose, + handleScrollToAnchor, +}) => { const [name, setName] = useState(""); const [phone, setPhone] = useState("+7"); const [country, setCountry] = useState("Европа"); @@ -64,9 +70,7 @@ const Feedback: React.FC = ({ open, onClose, handleScrollToAnchor React.useEffect(() => { if (open) { - window.history.pushState(null, '', '#feedback'); - } else if (!open && !isMobile && handleScrollToAnchor) { - handleScrollToAnchor('#main'); + window.history.pushState(null, "", "#feedback"); } }, [open, handleScrollToAnchor]); @@ -79,10 +83,10 @@ const Feedback: React.FC = ({ open, onClose, handleScrollToAnchor return; } console.log({ name, phone, country, budget, description, agreeToPolicy }); - + onClose(); if (handleScrollToAnchor) { - handleScrollToAnchor('#main'); + handleScrollToAnchor("#main"); } }; @@ -98,15 +102,17 @@ const Feedback: React.FC = ({ open, onClose, handleScrollToAnchor "& .MuiOutlinedInput-root, fieldset": { borderRadius: isMobile ? "3vw" : "1.5vw", fontSize: isMobile ? "2.8vw" : "1.25vw", + fontFamily: "Unbounded", }, "& .MuiInputLabel-root": { fontSize: isMobile ? "2.8vw" : "1.25vw", + fontFamily: "Unbounded", transform: isMobile - ? "translate(2.5vw, 3.1vw) scale(1)" + ? "translate(2.5vw, 2.1vw) scale(1)" : "translate(1vw, 1.1vw) scale(1)", "&.MuiInputLabel-shrink": { transform: isMobile - ? "translate(3vw, -1.6vw) scale(0.75)" + ? "translate(3.7vw, -1.6vw) scale(0.75)" : "translate(0.9vw, -0.8vw) scale(0.75)", }, "&.Mui-focused": { @@ -114,59 +120,45 @@ const Feedback: React.FC = ({ open, onClose, handleScrollToAnchor }, }, "& .MuiInputBase-input": { - fontSize: isMobile ? "2.8vw" : "1.25vw", - padding: isMobile ? "3vw 2.5vw" : "1vw 1.5vw", + fontSize: isMobile ? "2.5vw" : "1.25vw", + padding: isMobile ? "2vw 2.5vw" : "1vw 1.5vw", }, }; const handlePhoneChange = (event: React.ChangeEvent) => { - // Убедимся, что +7 всегда присутствует if (event.target.value.startsWith("+7")) { setPhone(event.target.value); } }; return ( - { - onClose(); - if (handleScrollToAnchor) handleScrollToAnchor('#main'); - }} - id="feedback" - > - + e.stopPropagation()} // предотвращаем закрытие при клике на контент > - { - onClose(); - if (handleScrollToAnchor) handleScrollToAnchor('#main'); - }} + = ({ open, onClose, handleScrollToAnchor Оставьте заявку - + И наш менеджер свяжется с вами для уточнения деталей заказа @@ -216,7 +212,6 @@ const Feedback: React.FC = ({ open, onClose, handleScrollToAnchor }} /> - {/* Поле с телефоном как на скриншоте */} = ({ open, onClose, handleScrollToAnchor id="phone-input" value={phone} onChange={handlePhoneChange} - inputComponent={TextMaskCustom as unknown as React.ComponentType} + inputComponent={ + TextMaskCustom as unknown as React.ComponentType< + import("@mui/material").InputBaseComponentProps + > + } label="Ваш телефон*" notched sx={{ @@ -256,10 +255,13 @@ const Feedback: React.FC = ({ open, onClose, handleScrollToAnchor /> - + Из какой страны привезти автомобиль? @@ -285,7 +287,10 @@ const Feedback: React.FC = ({ open, onClose, handleScrollToAnchor } label={ Европа @@ -311,7 +316,10 @@ const Feedback: React.FC = ({ open, onClose, handleScrollToAnchor } label={ США @@ -337,7 +345,10 @@ const Feedback: React.FC = ({ open, onClose, handleScrollToAnchor } label={ Китай @@ -363,7 +374,10 @@ const Feedback: React.FC = ({ open, onClose, handleScrollToAnchor } label={ Корея @@ -380,7 +394,11 @@ const Feedback: React.FC = ({ open, onClose, handleScrollToAnchor Какой у вас бюджет на автомобиль? @@ -406,7 +424,10 @@ const Feedback: React.FC = ({ open, onClose, handleScrollToAnchor } label={ до 3 млн @@ -417,7 +438,7 @@ const Feedback: React.FC = ({ open, onClose, handleScrollToAnchor }} /> = ({ open, onClose, handleScrollToAnchor } label={ 3-5 млн @@ -458,7 +482,10 @@ const Feedback: React.FC = ({ open, onClose, handleScrollToAnchor } label={ 5-10 млн @@ -469,7 +496,7 @@ const Feedback: React.FC = ({ open, onClose, handleScrollToAnchor }} /> = ({ open, onClose, handleScrollToAnchor } label={ 10+ млн @@ -524,6 +554,7 @@ const Feedback: React.FC = ({ open, onClose, handleScrollToAnchor py: "0.9vw", borderRadius: isMobile ? "3vw" : "1vw", fontSize: isMobile ? "3.5vw" : "1.25vw", + fontFamily: "Unbounded", "&:hover": { bgcolor: "#a42517" }, textTransform: "none", }} @@ -538,10 +569,7 @@ const Feedback: React.FC = ({ open, onClose, handleScrollToAnchor = ({ open, onClose, handleScrollToAnchor label={ Я подтверждаю, что ознакомлен{" "} = ({ open, onClose, handleScrollToAnchor cursor: "pointer", textDecoration: "underline", fontSize: isMobile ? "2.5vw" : "1vw", + fontFamily: "Unbounded", }} > с политикой конфиденциальности @@ -587,8 +619,8 @@ const Feedback: React.FC = ({ open, onClose, handleScrollToAnchor - - + + ); }; diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 06fa687..38aa113 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -21,7 +21,7 @@ const Header = () => { const [scrolled, setScrolled] = useState(false); const menuItems = [ - { title: "О нас", anchor: "#about" }, + { title: "О нас", anchor: "#about-us" }, { title: "Калькулятор", anchor: "#calculator" }, { title: "Отзывы", anchor: "#reviews" }, { title: "Контакты", anchor: "#contacts" }, @@ -29,7 +29,7 @@ const Header = () => { { title: "Команда", anchor: "#team" }, { title: "Доставленные авто", anchor: "#delivered" }, { title: "Этапы работы", anchor: "#stages" }, - { title: " ", anchor: "#main"}, + { title: " ", anchor: "#main" }, ]; // Отслеживание скролла @@ -48,21 +48,23 @@ const Header = () => { }; }, []); - const toggleDrawer = (open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => { - if ( - event.type === "keydown" && - ((event as React.KeyboardEvent).key === "Tab" || (event as React.KeyboardEvent).key === "Shift") - ) { - return; - } - setDrawerOpen(open); - }; - - // Используем глобальную функцию из utils - const handleScrollToAnchor = (anchor: string) => { - scrollToAnchor(anchor, setDrawerOpen, isMobile); + const toggleDrawer = + (open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => { + if ( + event.type === "keydown" && + ((event as React.KeyboardEvent).key === "Tab" || + (event as React.KeyboardEvent).key === "Shift") + ) { + return; + } + setDrawerOpen(open); }; + // Используем глобальную функцию из utils + const handleScrollToAnchor = (anchor: string) => { + scrollToAnchor(anchor, setDrawerOpen, isMobile); + }; + return ( <> { "&:hover": { scale: 1.1 }, transition: "all 0.4s ease", }} - > - + > {isMobile ? ( { {/* Мобильное меню */} - + {menuItems.map((item, index) => ( - handleScrollToAnchor(item.anchor)} sx={{ cursor: "pointer" }} > @@ -188,7 +185,11 @@ const Header = () => { - setFeedbackOpen(false)} handleScrollToAnchor={handleScrollToAnchor} /> + setFeedbackOpen(false)} + handleScrollToAnchor={handleScrollToAnchor} + /> ); }; diff --git a/src/pages/AboutUsPage.tsx b/src/pages/AboutUsPage.tsx new file mode 100644 index 0000000..1dc735c --- /dev/null +++ b/src/pages/AboutUsPage.tsx @@ -0,0 +1,90 @@ +import React from "react"; +import { Box, Typography } from "@mui/material"; +import { useResponsive } from "../theme/useResponsive"; + +function AboutUsPage() { + const { isMobile } = useResponsive(); + + return ( + + + + КТО МЫ? + + + + АВТОБРО + + + + Мы предлагаем широкий выбор автомобилей на любой вкус и бюджет. Мы + привозим новые машины под заказ напрямую от производителей, помогая + клиентам получить желаемый автомобиль в нужной комплектации. + + + + Также у нас есть проверенные б/у автомобили с прозрачной историей и + готовые варианты из наличия – вы можете уехать на новом авто уже в + день покупки. + + + + Наш автосалон работает для тех, кто ценит надежность, выгодные условия + и индивидуальный подход. Оставьте заявку, и мы подберем для вас + идеальный вариант! + + + + ); +} + +export default AboutUsPage; diff --git a/src/pages/CarsPage.tsx b/src/pages/CarsPage.tsx new file mode 100644 index 0000000..a8f5f40 --- /dev/null +++ b/src/pages/CarsPage.tsx @@ -0,0 +1,550 @@ +import React, { useState, useEffect, useRef } from "react"; +import { Box, Typography } from "@mui/material"; +import { useResponsive } from "../theme/useResponsive"; +import Feedback from "../components/Feedback"; +import { fetchCars, getImageUrl, type Car } from "../utils/api"; +import KeyboardDoubleArrowLeftIcon from "@mui/icons-material/KeyboardDoubleArrowLeft"; +import KeyboardDoubleArrowRightIcon from "@mui/icons-material/KeyboardDoubleArrowRight"; + +function CarsPage() { + const { isMobile } = useResponsive(); + const [cars, setCars] = useState([]); + const [loading, setLoading] = useState(true); + const [feedbackOpen, setFeedbackOpen] = useState(false); + const scrollContainerRef = useRef(null); + const scrollTrackRef = useRef(null); + const scrollThumbRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); + const [thumbWidth, setThumbWidth] = useState(30); // в процентах + + useEffect(() => { + const loadCars = async () => { + try { + const response = await fetchCars(); + setCars(response.cars); + } catch (error) { + console.error("Ошибка при загрузке данных:", error); + } finally { + setLoading(false); + } + }; + + loadCars(); + }, []); + + // Обновление размера ползунка скролл-бара + useEffect(() => { + const updateThumbWidth = () => { + if (scrollContainerRef.current && scrollTrackRef.current) { + const { scrollWidth, clientWidth } = scrollContainerRef.current; + const thumbWidthPercent = (clientWidth / scrollWidth) * 100; + setThumbWidth(Math.max(10, thumbWidthPercent)); // Минимальная ширина 10% + } + }; + + updateThumbWidth(); + window.addEventListener("resize", updateThumbWidth); + return () => window.removeEventListener("resize", updateThumbWidth); + }, [cars]); + + // Синхронизация положения скролл-бара со скроллом контейнера + useEffect(() => { + const syncScrollThumb = () => { + if ( + scrollContainerRef.current && + scrollTrackRef.current && + scrollThumbRef.current + ) { + const { scrollLeft, scrollWidth, clientWidth } = + scrollContainerRef.current; + const trackWidth = scrollTrackRef.current.clientWidth; + const scrollPercent = scrollLeft / (scrollWidth - clientWidth); + + // Учитываем отступы 0.5vw с обеих сторон + const minOffset = + parseFloat(getComputedStyle(document.documentElement).fontSize) * + (isMobile ? 0.25 : 0.5); + const maxOffset = + trackWidth - (trackWidth * thumbWidth) / 100 - minOffset; + + // Расчет позиции с учетом отступов + const thumbLeft = minOffset + scrollPercent * (maxOffset - minOffset); + scrollThumbRef.current.style.left = `${thumbLeft}px`; + } + }; + + const scrollContainer = scrollContainerRef.current; + if (scrollContainer) { + scrollContainer.addEventListener("scroll", syncScrollThumb); + } + + return () => { + if (scrollContainer) { + scrollContainer.removeEventListener("scroll", syncScrollThumb); + } + }; + }, [thumbWidth]); + + // Обработчики перетаскивания скролл-бара + const handleDragStart = (e: React.MouseEvent) => { + e.preventDefault(); + setIsDragging(true); + }; + + // Добавьте эти обработчики для сенсорных событий + const handleTouchStart = (e: React.TouchEvent) => { + e.preventDefault(); // Предотвращаем стандартное поведение + setIsDragging(true); + + // Сохраняем текущую позицию касания + if (e.touches.length > 0) { + const touch = e.touches[0]; + handleDragMove(touch); // Переиспользуем функцию для перетаскивания + } + }; + + const handleDragMove = (e: MouseEvent | Touch) => { + if (!isDragging || !scrollTrackRef.current || !scrollContainerRef.current) + return; + + const trackRect = scrollTrackRef.current.getBoundingClientRect(); + const thumbWidthPx = trackRect.width * (thumbWidth / 100); + const trackWidth = trackRect.width - thumbWidthPx; + + // Используем clientX, которое есть как у MouseEvent, так и у Touch + let clickPosition = + (e.clientX - trackRect.left - thumbWidthPx / 2) / trackWidth; + clickPosition = Math.max(0, Math.min(1, clickPosition)); + + // Устанавливаем скролл контейнера + const { scrollWidth, clientWidth } = scrollContainerRef.current; + scrollContainerRef.current.scrollLeft = + clickPosition * (scrollWidth - clientWidth); + }; + + const handleDragEnd = () => { + setIsDragging(false); + }; + + // Обновите функцию handleDragMove для работы как с мышью, так и с сенсорными событиями + const handleTouchMove = (e: TouchEvent) => { + e.preventDefault(); // Предотвращаем скроллинг страницы + + if (isDragging && e.touches.length > 0) { + const touch = e.touches[0]; + handleDragMove(touch); // Переиспользуем функцию для перетаскивания + } + }; + + const handleTouchEnd = (e: TouchEvent) => { + e.preventDefault(); + setIsDragging(false); + }; + + // Обновите useEffect для добавления обработчиков сенсорных событий + useEffect(() => { + if (isDragging) { + // Вместо простого скрытия overflow, сохраняем ширину до блокировки + const scrollbarWidth = + window.innerWidth - document.documentElement.clientWidth; + + // Блокируем скролл страницы + document.body.style.overflow = "hidden"; + + // Добавляем padding-right для компенсации исчезнувшего скроллбара + document.body.style.paddingRight = `${scrollbarWidth}px`; + + window.addEventListener("mousemove", handleDragMove); + window.addEventListener("mouseup", handleDragEnd); + window.addEventListener("touchmove", handleTouchMove, { passive: false }); + window.addEventListener("touchend", handleTouchEnd, { passive: false }); + } else { + // Восстанавливаем скроллинг и убираем padding + document.body.style.overflow = ""; + document.body.style.paddingRight = ""; + + window.removeEventListener("mousemove", handleDragMove); + window.removeEventListener("mouseup", handleDragEnd); + window.removeEventListener("touchmove", handleTouchMove); + window.removeEventListener("touchend", handleTouchEnd); + } + + return () => { + // Не забываем сбросить стили при размонтировании + document.body.style.overflow = ""; + document.body.style.paddingRight = ""; + + window.removeEventListener("mousemove", handleDragMove); + window.removeEventListener("mouseup", handleDragEnd); + window.removeEventListener("touchmove", handleTouchMove); + window.removeEventListener("touchend", handleTouchEnd); + }; + }, [isDragging]); + + // Обработчик клика по треку скролл-бара + const handleTrackClick = (e: React.MouseEvent) => { + if (!scrollTrackRef.current || !scrollContainerRef.current) return; + + const trackRect = scrollTrackRef.current.getBoundingClientRect(); + const thumbWidthPx = trackRect.width * (thumbWidth / 100); + const trackWidth = trackRect.width - thumbWidthPx; + + // Рассчитываем положение клика относительно трека с учетом ширины ползунка + let clickPosition = + (e.clientX - trackRect.left - thumbWidthPx / 2) / trackWidth; + clickPosition = Math.max(0, Math.min(1, clickPosition)); + + // Устанавливаем скролл контейнера + const { scrollWidth, clientWidth } = scrollContainerRef.current; + scrollContainerRef.current.scrollLeft = + clickPosition * (scrollWidth - clientWidth); + }; + + const handleOpenFeedback = () => { + setFeedbackOpen(true); + }; + + const handleCloseFeedback = () => { + setFeedbackOpen(false); + }; + + // Функция для прокрутки влево + const scrollLeft = () => { + if (scrollContainerRef.current) { + // Найти текущую видимую карточку + const container = scrollContainerRef.current; + const scrollPosition = container.scrollLeft; + const cardWidth = isMobile ? 80 : 25; + const gap = 2; + const vwToPx = (vw: number) => (window.innerWidth * vw) / 100; + + // Рассчитать индекс предыдущей карточки + const currentIndex = Math.round(scrollPosition / vwToPx(cardWidth + gap)); + const prevIndex = Math.max(0, currentIndex - 1); + + // Прокрутить к предыдущей карточке + if (container.children[prevIndex]) { + container.children[prevIndex].scrollIntoView({ + behavior: "smooth", + block: "nearest", + inline: "start", + }); + + // Добавляем дополнительный сдвиг на 0.5vw для ПК версии + if (!isMobile) { + setTimeout(() => { + container.scrollLeft -= vwToPx(0.5); + }, 50); + } + } + } + }; + + // Функция для прокрутки вправо + const scrollRight = () => { + if (scrollContainerRef.current) { + // Найти текущую видимую карточку + const container = scrollContainerRef.current; + const scrollPosition = container.scrollLeft; + const cardWidth = isMobile ? 80 : 25; + const gap = 2; + const vwToPx = (vw: number) => (window.innerWidth * vw) / 100; + + // Рассчитать индекс следующей карточки + const currentIndex = Math.round(scrollPosition / vwToPx(cardWidth + gap)); + const nextIndex = Math.min(cars.length - 1, currentIndex + 1); + + // Прокрутить к следующей карточке + if (container.children[nextIndex]) { + container.children[nextIndex].scrollIntoView({ + behavior: "smooth", + block: "nearest", + inline: "start", + }); + + if (!isMobile && nextIndex !== cars.length - 1) { + setTimeout(() => { + container.scrollLeft -= vwToPx(0.5); + }, 50); + } + } + } + }; + + return ( + + + + АВТОМОБИЛИ В НАЛИЧИИ + + + {loading ? ( + Загрузка... + ) : ( + <> + {/* Контейнер для карточек с горизонтальным скроллом */} + + {cars.map((car, index) => ( + + + + + {car.name} + + + Год выпуска {car.year} + + + Пробег {car.mileage.toLocaleString()} км. + + + + {car.price.toLocaleString()}₽ + + + + ))} + + + + + + + + {/* Индикатор скролла */} + + + + + {/* Кнопки */} + + + Наличие в Telegram + + + Оставить заявку + + + + )} + + + {}} + /> + + ); +} + +export default CarsPage; diff --git a/src/pages/DeliveryPage.tsx b/src/pages/DeliveryPage.tsx new file mode 100644 index 0000000..c816157 --- /dev/null +++ b/src/pages/DeliveryPage.tsx @@ -0,0 +1,151 @@ +import React, { useState } from "react"; +import { Box, Typography } from "@mui/material"; +import { useResponsive } from "../theme/useResponsive"; +import usa from "../assets/emoji/united-states.png"; +import china from "../assets/emoji/china.png"; +import korea from "../assets/emoji/korea.png"; +import Feedback from "../components/Feedback"; +import { scrollToAnchor } from "../utils/scrollUtils"; + +function DeliveryPage() { + const { isMobile } = useResponsive(); + const [feedbackOpen, setFeedbackOpen] = useState(false); + const setDrawerOpen = () => {}; + + const handleOpenFeedback = () => { + setFeedbackOpen(true); + }; + + const handleCloseFeedback = () => { + setFeedbackOpen(false); + }; + + const handleScrollToAnchor = (anchor: string) => { + scrollToAnchor(anchor, setDrawerOpen, isMobile); + }; + + return ( + + + + Мы доставляем автомобили из{" "} + США, Китая и Кореи + + + usa + china + korea + + + + + Не знаете какой авто выбрать? + + + Подберите авто прямо сейчас! + + + Подобрать авто + + + + + + ); +} + +export default DeliveryPage; diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx index 3cd932b..6e92c68 100644 --- a/src/pages/MainPage.tsx +++ b/src/pages/MainPage.tsx @@ -3,10 +3,14 @@ import { Button, Box, Typography, IconButton } from "@mui/material"; import Feedback from "../components/Feedback"; import car from "../../src/assets/icon/car.png"; import { useResponsive } from "../theme/useResponsive"; -import TelegramIcon from '@mui/icons-material/Telegram'; -import VkIcon from '@mui/icons-material/Facebook'; -import WhatsAppIcon from '@mui/icons-material/WhatsApp'; +import TelegramIcon from "@mui/icons-material/Telegram"; +import VkIcon from "@mui/icons-material/Facebook"; +import WhatsAppIcon from "@mui/icons-material/WhatsApp"; import { scrollToAnchor } from "../utils/scrollUtils"; +import AboutUsPage from "./AboutUsPage"; +import StagesPage from "./StagesPage"; +import CarsPage from "./CarsPage"; +import DeliveryPage from "./DeliveryPage"; function MainPage() { const [feedbackOpen, setFeedbackOpen] = useState(false); @@ -26,172 +30,196 @@ function MainPage() { }; return ( - + - - АВТО - - - БРО - - logo - - - - - - + logo - position: "absolute", - right: "1vw", - top: isMobile ? "20.3vw" : "15.3vw", - display: "flex", - flexDirection: "column", - gap: "1vw", - zIndex: 1, - }}> - + + + + + + - - - - - + - + + + + + + ); } diff --git a/src/pages/StagesPage.tsx b/src/pages/StagesPage.tsx new file mode 100644 index 0000000..988eac1 --- /dev/null +++ b/src/pages/StagesPage.tsx @@ -0,0 +1,449 @@ +import React, { useState } from "react"; +import { Box, Typography } from "@mui/material"; +import { useResponsive } from "../theme/useResponsive"; +import Feedback from "../components/Feedback"; +import emoji1 from "../assets/emoji/1.png"; +import emoji2 from "../assets/emoji/2.png"; +import emoji3 from "../assets/emoji/3.png"; +import emoji4 from "../assets/emoji/4.png"; +import emoji5 from "../assets/emoji/5.png"; + +function StagesPage() { + const { isMobile } = useResponsive(); + const [feedbackOpen, setFeedbackOpen] = useState(false); + + const handleOpenFeedback = () => { + setFeedbackOpen(true); + }; + + const handleCloseFeedback = () => { + setFeedbackOpen(false); + const circle = document.querySelector(".slider-circle") as HTMLElement; + if (circle) { + // Удаляем inline-стиль вместо его установки + circle.style.removeProperty("left"); + } + }; + + const stageCards = [ + { + number: "1", + emoji: emoji1, + title: "Знакомство с компанией и предварительный договор", + items: [ + "Консультация", + "Подбор автомобиля", + "предварительный расчет стоимости", + "Заключение предварительного договора и внесение депозита 10000₽ (100 000 руб — входит в стоимость автомобиля)", + ], + }, + { + number: "2", + emoji: emoji2, + title: "Подбор, выездная диагностика и выкуп автомобиля", + items: [ + "Поиск - подбираем авто по вашим параметрам.", + "Проверка – выездная экспертиза с отчетом.", + "Покупка – безопасный выкуп.", + "Договор – фиксируем цену (авто + доставка + таможня), предоплата 30-70%.", + ], + }, + { + number: "3", + emoji: emoji3, + title: "Логистика", + items: [ + "Привезем автомобиль в установленные сроки: Китай от 30 дней, Европа от 30 дней, США от 60 дней, Корея от 45 дней", + "Выплатим компенсацию за каждый день задержки", + ], + }, + { + number: "4", + emoji: emoji4, + title: "Таможенная очистка и получение эПТС", + items: [ + "В зависимости от страны экспорта таможенная очистка проходит в разных таможенных пунктах (благодаря нашему опыту мы всегда делаем предварительные расчеты, где выгоднее для клиента проводить таможенное оформление)", + ], + }, + { + number: "5", + emoji: emoji5, + title: "Вручение", + items: [ + "Автомобиль вручается с полным комплектом документов в нашем автосалоне в Москве", + "Собственный парк эвакуаторов позволяет организовать доставку автомобиля с полным комплектом документов до двери вашего дома", + ], + }, + ]; + + return ( + + + + ЭТАПЫ РАБОТЫ + + + {isMobile ? ( + + {stageCards.map((card, index) => ( + + + {`Этап + + {card.number}. {card.title} + + + + + {card.items.map((item, idx) => ( + + + • {item} + + + ))} + + + ))} + + { + handleOpenFeedback(); + const circle = e.currentTarget.querySelector( + ".slider-circle" + ) as HTMLElement; + if (circle) { + circle.style.left = "18.2vw"; + } + }} + sx={{ + background: "linear-gradient(to bottom, #C27664, #f7c6bc)", + borderRadius: isMobile ? "4.5vw" : "1.5vw", + color: "#ffffff", + display: "flex", + flexDirection: "column", + width: isMobile ? "70vw" : "24vw", + maxWidth: isMobile ? "70vw" : "24vw", + height: isMobile ? "85vw" : "29vw", + maxHeight: isMobile ? "85vw" : "29vw", + boxShadow: isMobile + ? "-1.5vw 1.5vw 2vw rgba(0, 0, 0, 0.3), inset 1.5vw 1.5vw 1.7vw rgba(255, 255, 255, 0.3)" + : "-0.5vw 0.5vw 2vw rgba(0, 0, 0, 0.3), inset 0.5vw 0.5vw 0.7vw rgba(255, 255, 255, 0.3)", + cursor: "pointer", + transition: "all 0.3s ease", + "&:hover": { + boxShadow: + "-0.5vw 0.5vw 2.5vw rgba(0, 0, 0, 0.4), inset 0.5vw 0.5vw 0.7vw rgba(255, 255, 255, 0.3)", + "& .slider-circle": { + left: "18.2vw", + }, + }, + }} + > + + + Есть вопросы? + + + + Оставьте заявку прямо сейчас + + + + + + + + + ) : ( + + {stageCards.map((card, index) => ( + + + {`Этап + + {card.number}. {card.title} + + + + + {card.items.map((item, idx) => ( + + + • {item} + + + ))} + + + ))} + + { + handleOpenFeedback(); + const circle = e.currentTarget.querySelector( + ".slider-circle" + ) as HTMLElement; + if (circle) { + circle.style.left = "18.2vw"; + } + }} + sx={{ + background: "linear-gradient(to bottom, #C27664, #f7c6bc)", + borderRadius: "1.5vw", + color: "#ffffff", + display: "flex", + flexDirection: "column", + width: "24vw", + maxWidth: "24vw", + height: "29vw", + maxHeight: "29vw", + boxShadow: + "-0.5vw 0.5vw 2vw rgba(0, 0, 0, 0.3), inset 0.5vw 0.5vw 0.7vw rgba(255, 255, 255, 0.3)", + cursor: "pointer", + transition: "all 0.3s ease", + "&:hover": { + boxShadow: + "-0.5vw 0.5vw 2.5vw rgba(0, 0, 0, 0.4), inset 0.5vw 0.5vw 0.7vw rgba(255, 255, 255, 0.3)", + "& .slider-circle": { + left: "18.2vw", + }, + }, + }} + > + + + Есть вопросы? + + + + Оставьте заявку прямо сейчас + + + + + + + + + )} + + + {}} // Если нужен пустой обработчик для совместимости + /> + + ); +} + +export default StagesPage; diff --git a/src/utils/api.ts b/src/utils/api.ts new file mode 100644 index 0000000..dc84700 --- /dev/null +++ b/src/utils/api.ts @@ -0,0 +1,131 @@ +// utils/api.ts + +// Типы данных +export interface Car { + id: number; + make: string; + model: string; + year: number; + mileage: number; + price: number; + image: string; +} + +export interface CarsResponse { + cars: Car[]; + total: number; +} + +export interface CarResponse { + car: Car; +} + +// Базовый URL API +const API_BASE_URL = 'http://localhost:8000'; + +// Общая функция для обработки ошибок +const handleApiError = (error: any): never => { + console.error('API Error:', error); + throw new Error(error?.message || 'Произошла ошибка при запросе к API'); +}; + +// Получение списка автомобилей +export const fetchCars = async (skip = 0, limit = 100): Promise => { + try { + const response = await fetch(`${API_BASE_URL}/cars?skip=${skip}&limit=${limit}`); + if (!response.ok) { + throw new Error(`Ошибка HTTP: ${response.status}`); + } + return await response.json(); + } catch (error) { + return handleApiError(error); + } +}; + +// Получение одного автомобиля по ID +export const fetchCarById = async (carId: number): Promise => { + try { + const response = await fetch(`${API_BASE_URL}/cars/${carId}`); + if (!response.ok) { + throw new Error(`Ошибка HTTP: ${response.status}`); + } + return await response.json(); + } catch (error) { + return handleApiError(error); + } +}; + +// Добавление нового автомобиля +export const createCar = async (carData: Omit, image?: File): Promise => { + try { + const formData = new FormData(); + formData.append('car_data', JSON.stringify(carData)); + + if (image) { + formData.append('image', image); + } + + const response = await fetch(`${API_BASE_URL}/cars`, { + method: 'POST', + body: formData, + }); + + if (!response.ok) { + throw new Error(`Ошибка HTTP: ${response.status}`); + } + + return await response.json(); + } catch (error) { + return handleApiError(error); + } +}; + +// Обновление автомобиля +export const updateCar = async ( + carId: number, + carData: Partial>, + image?: File +): Promise => { + try { + const formData = new FormData(); + formData.append('car_data', JSON.stringify(carData)); + + if (image) { + formData.append('image', image); + } + + const response = await fetch(`${API_BASE_URL}/cars/${carId}`, { + method: 'PUT', + body: formData, + }); + + if (!response.ok) { + throw new Error(`Ошибка HTTP: ${response.status}`); + } + + return await response.json(); + } catch (error) { + return handleApiError(error); + } +}; + +// Удаление автомобиля +export const deleteCar = async (carId: number): Promise => { + try { + const response = await fetch(`${API_BASE_URL}/cars/${carId}`, { + method: 'DELETE', + }); + + if (!response.ok) { + throw new Error(`Ошибка HTTP: ${response.status}`); + } + } catch (error) { + return handleApiError(error); + } +}; + +// Вспомогательная функция для формирования полного URL для изображения +export const getImageUrl = (imagePath: string | null): string => { + if (!imagePath) return '/placeholder.jpg'; + return `${API_BASE_URL}${imagePath}`; +}; diff --git a/src/utils/scrollUtils.ts b/src/utils/scrollUtils.ts index 64468e4..3c03451 100644 --- a/src/utils/scrollUtils.ts +++ b/src/utils/scrollUtils.ts @@ -1,5 +1,5 @@ /** - * Плавная прокрутка к указанному якорю + * Плавная прокрутка к указанному якорю с учетом высоты хедера * @param anchor - идентификатор якоря, например "#about" * @param setDrawerOpen - опциональная функция для закрытия мобильного меню * @param isMobile - флаг мобильного устройства @@ -11,11 +11,53 @@ export const scrollToAnchor = ( ): void => { const element = document.querySelector(anchor); if (element) { - element.scrollIntoView({ behavior: "smooth" }); + // Получаем высоту хедера (в соответствии с размерами в Header.tsx) + const headerHeight = isMobile ? window.innerWidth * 0.15 : window.innerWidth * 0.10; // 15vw или 10vw + + // Получаем позицию элемента относительно верха страницы + const elementPosition = element.getBoundingClientRect().top + window.scrollY; + + // Целевая позиция с учетом высоты хедера + const targetPosition = elementPosition - headerHeight; + // Обновляем URL с хешем без перезагрузки страницы window.history.pushState(null, '', anchor); + + // Реализация плавного скролла с анимацией + smoothScrollTo(targetPosition); } + if (isMobile && setDrawerOpen) { setDrawerOpen(false); } -}; \ No newline at end of file +}; + +/** + * Функция для плавного скролла с анимацией + * @param targetPosition - конечная позиция скролла + */ +function smoothScrollTo(targetPosition: number, duration: number = 500) { + const startPosition = window.scrollY; + const distance = targetPosition - startPosition; + const startTime = performance.now(); + + function step(currentTime: number) { + const elapsedTime = currentTime - startTime; + + if (elapsedTime >= duration) { + window.scrollTo(0, targetPosition); + return; + } + + // Функция плавности - easeInOutQuad + const progress = elapsedTime / duration; + const easeProgress = progress < 0.5 + ? 2 * progress * progress + : 1 - Math.pow(-2 * progress + 2, 2) / 2; + + window.scrollTo(0, startPosition + distance * easeProgress); + requestAnimationFrame(step); + } + + requestAnimationFrame(step); +} \ No newline at end of file