add new pages

This commit is contained in:
aurinex
2025-07-09 17:29:31 +05:00
parent 824a79a117
commit 43d4cbcb89
17 changed files with 1703 additions and 229 deletions

BIN
src/assets/emoji/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
src/assets/emoji/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
src/assets/emoji/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
src/assets/emoji/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
src/assets/emoji/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
src/assets/emoji/china.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
src/assets/emoji/korea.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -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<FeedbackProps> = ({ open, onClose, handleScrollToAnchor }) => {
const Feedback: React.FC<FeedbackProps> = ({
open,
onClose,
handleScrollToAnchor,
}) => {
const [name, setName] = useState("");
const [phone, setPhone] = useState("+7");
const [country, setCountry] = useState("Европа");
@ -64,9 +70,7 @@ const Feedback: React.FC<FeedbackProps> = ({ 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]);
@ -82,7 +86,7 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
onClose();
if (handleScrollToAnchor) {
handleScrollToAnchor('#main');
handleScrollToAnchor("#main");
}
};
@ -98,15 +102,17 @@ const Feedback: React.FC<FeedbackProps> = ({ 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<FeedbackProps> = ({ 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<HTMLInputElement>) => {
// Убедимся, что +7 всегда присутствует
if (event.target.value.startsWith("+7")) {
setPhone(event.target.value);
}
};
return (
<Box
sx={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
display: open ? "flex" : "none",
alignItems: "center",
justifyContent: "center",
zIndex: 1300, // высокий z-index как у диалога
bgcolor: "rgba(0, 0, 0, 0.5)", // затемнение фона
color: "black",
}}
onClick={() => {
onClose();
if (handleScrollToAnchor) handleScrollToAnchor('#main');
}}
id="feedback"
>
<Box
sx={{
position: "relative",
bgcolor: "background.paper",
<Dialog
open={open}
onClose={onClose}
maxWidth="md"
fullWidth
PaperProps={{
sx: {
borderRadius: isMobile ? "3vw" : "1.5vw",
p: isMobile ? "6vw" : "4vw",
maxWidth: isMobile ? "70vw" : "40vw",
maxHeight: "75vh",
overflow: "hidden",
boxShadow: 24, // тень как у диалога
margin: "auto", // для дополнительной центровки
margin: "auto",
maxWidth: isMobile ? "85vw" : "38vw",
position: "fixed",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
overflowY: "hidden",
},
}}
>
<DialogContent
sx={{
padding: isMobile ? "4vw" : "3vw",
overflowY: "hidden",
}}
onClick={(e) => e.stopPropagation()} // предотвращаем закрытие при клике на контент
>
<Box>
<IconButton
onClick={() => {
onClose();
if (handleScrollToAnchor) handleScrollToAnchor('#main');
}}
onClick={onClose}
sx={{
position: "absolute",
right: "1vw",
@ -181,20 +173,24 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
<Typography
variant="h5"
fontWeight="bold"
sx={{ fontSize: isMobile ? "5vw" : "1.9vw" }}
sx={{
fontSize: isMobile ? "5vw" : "1.9vw",
fontFamily: "Unbounded",
}}
>
Оставьте заявку
</Typography>
</Box>
<Box sx={{}}>
<Box>
<Typography
variant="body1"
sx={{
textAlign: "center",
fontSize: isMobile ? "3.5vw" : "1.25vw",
maxWidth: "65vw",
maxWidth: "100%",
margin: "auto",
fontFamily: "Unbounded",
}}
>
И наш менеджер свяжется с вами для уточнения деталей заказа
@ -216,7 +212,6 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
}}
/>
{/* Поле с телефоном как на скриншоте */}
<FormControl
fullWidth
variant="outlined"
@ -244,7 +239,11 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
id="phone-input"
value={phone}
onChange={handlePhoneChange}
inputComponent={TextMaskCustom as unknown as React.ComponentType<import('@mui/material').InputBaseComponentProps>}
inputComponent={
TextMaskCustom as unknown as React.ComponentType<
import("@mui/material").InputBaseComponentProps
>
}
label="Ваш телефон*"
notched
sx={{
@ -256,10 +255,13 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
/>
</FormControl>
<Box sx={{ mb: isMobile ? "3vw" : "0.5vw" }}>
<Box sx={{ mb: isMobile ? "2vw" : "0.5vw" }}>
<Typography
variant="body2"
sx={{ mb: "0.5vw", fontSize: isMobile ? "3.5vw" : "1.1vw" }}
sx={{
fontSize: isMobile ? "3vw" : "1vw",
fontFamily: "Unbounded",
}}
>
Из какой страны привезти автомобиль?
</Typography>
@ -285,7 +287,10 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
}
label={
<Typography
sx={{ fontSize: isMobile ? "3vw" : "1.1vw" }}
sx={{
fontSize: isMobile ? "3vw" : "1.1vw",
fontFamily: "Unbounded",
}}
>
Европа
</Typography>
@ -311,7 +316,10 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
}
label={
<Typography
sx={{ fontSize: isMobile ? "3vw" : "1.1vw" }}
sx={{
fontSize: isMobile ? "3vw" : "1.1vw",
fontFamily: "Unbounded",
}}
>
США
</Typography>
@ -337,7 +345,10 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
}
label={
<Typography
sx={{ fontSize: isMobile ? "3vw" : "1.1vw" }}
sx={{
fontSize: isMobile ? "3vw" : "1.1vw",
fontFamily: "Unbounded",
}}
>
Китай
</Typography>
@ -363,7 +374,10 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
}
label={
<Typography
sx={{ fontSize: isMobile ? "3vw" : "1.1vw" }}
sx={{
fontSize: isMobile ? "3vw" : "1.1vw",
fontFamily: "Unbounded",
}}
>
Корея
</Typography>
@ -380,7 +394,11 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
<Box sx={{ mb: isMobile ? "2.5vw" : "1vw" }}>
<Typography
variant="body2"
sx={{ mb: "0.5vw", fontSize: isMobile ? "3.5vw" : "1.1vw" }}
sx={{
mb: "0.5vw",
fontSize: isMobile ? "3vw" : "1.1vw",
fontFamily: "Unbounded",
}}
>
Какой у вас бюджет на автомобиль?
</Typography>
@ -406,7 +424,10 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
}
label={
<Typography
sx={{ fontSize: isMobile ? "3vw" : "1.1vw" }}
sx={{
fontSize: isMobile ? "3vw" : "1.1vw",
fontFamily: "Unbounded",
}}
>
до 3 млн
</Typography>
@ -417,7 +438,7 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
}}
/>
<FormControlLabel
value="до 5 млн"
value="3-5 млн"
control={
<Radio
sx={{
@ -432,7 +453,10 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
}
label={
<Typography
sx={{ fontSize: isMobile ? "3vw" : "1.1vw" }}
sx={{
fontSize: isMobile ? "3vw" : "1.1vw",
fontFamily: "Unbounded",
}}
>
3-5 млн
</Typography>
@ -458,7 +482,10 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
}
label={
<Typography
sx={{ fontSize: isMobile ? "3vw" : "1.1vw" }}
sx={{
fontSize: isMobile ? "3vw" : "1.1vw",
fontFamily: "Unbounded",
}}
>
5-10 млн
</Typography>
@ -469,7 +496,7 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
}}
/>
<FormControlLabel
value="более 10 млн"
value="10+ млн"
control={
<Radio
sx={{
@ -484,7 +511,10 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
}
label={
<Typography
sx={{ fontSize: isMobile ? "3vw" : "1.1vw" }}
sx={{
fontSize: isMobile ? "3vw" : "1.1vw",
fontFamily: "Unbounded",
}}
>
10+ млн
</Typography>
@ -524,6 +554,7 @@ const Feedback: React.FC<FeedbackProps> = ({ 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<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
<Box
sx={{
mt: isMobile ? "2vw" : "1vw",
display: "flex",
alignItems: "center",
justifyContent: "center",
mt: isMobile ? "1vw" : "0.5vw",
}}
>
<FormControlLabel
@ -566,7 +594,10 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
label={
<Typography
variant="body2"
sx={{ fontSize: isMobile ? "2.5vw" : "1vw" }}
sx={{
fontSize: isMobile ? "2.5vw" : "1vw",
fontFamily: "Unbounded",
}}
>
Я подтверждаю, что ознакомлен{" "}
<Typography
@ -576,6 +607,7 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
cursor: "pointer",
textDecoration: "underline",
fontSize: isMobile ? "2.5vw" : "1vw",
fontFamily: "Unbounded",
}}
>
с политикой конфиденциальности
@ -587,8 +619,8 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
</Box>
</Box>
</Box>
</Box>
</Box>
</DialogContent>
</Dialog>
);
};

View File

@ -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" },
@ -48,10 +48,12 @@ const Header = () => {
};
}, []);
const toggleDrawer = (open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => {
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")
((event as React.KeyboardEvent).key === "Tab" ||
(event as React.KeyboardEvent).key === "Shift")
) {
return;
}
@ -98,8 +100,7 @@ const Header = () => {
"&:hover": { scale: 1.1 },
transition: "all 0.4s ease",
}}
>
</Box>
></Box>
{isMobile ? (
<Box
@ -154,11 +155,7 @@ const Header = () => {
<Box sx={{ height: isMobile ? "15vw" : "10vw" }} />
{/* Мобильное меню */}
<Drawer
anchor="right"
open={drawerOpen}
onClose={toggleDrawer(false)}
>
<Drawer anchor="right" open={drawerOpen} onClose={toggleDrawer(false)}>
<Box
sx={{ width: "58vw", bgcolor: "#2D2D2D", height: "100%" }}
role="presentation"
@ -188,7 +185,11 @@ const Header = () => {
</Box>
</Drawer>
<Feedback open={feedbackOpen} onClose={() => setFeedbackOpen(false)} handleScrollToAnchor={handleScrollToAnchor} />
<Feedback
open={feedbackOpen}
onClose={() => setFeedbackOpen(false)}
handleScrollToAnchor={handleScrollToAnchor}
/>
</>
);
};

90
src/pages/AboutUsPage.tsx Normal file
View File

@ -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 (
<Box
id="about-us"
sx={{
bgcolor: "white",
color: "black",
padding: "5vw",
marginTop: "5vw",
}}
>
<Box
sx={{
maxWidth: "80vw",
margin: "0 auto",
textAlign: "left",
}}
>
<Typography
component="h1"
sx={{
fontFamily: "Unbounded",
fontSize: isMobile ? "8vw" : "4rem",
fontWeight: "bold",
marginBottom: "1rem",
}}
>
КТО <span style={{ color: "#C27664" }}>МЫ?</span>
</Typography>
<Typography
component="h2"
sx={{
fontFamily: "Unbounded",
fontSize: isMobile ? "7vw" : "3.5rem",
fontWeight: "bold",
}}
>
АВТОБРО
</Typography>
<Typography
sx={{
fontSize: isMobile ? "4vw" : "1.5rem",
marginBottom: "2rem",
lineHeight: 1.6,
fontFamily: "Unbounded",
}}
>
Мы предлагаем широкий выбор автомобилей на любой вкус и бюджет. Мы
привозим новые машины под заказ напрямую от производителей, помогая
клиентам получить желаемый автомобиль в нужной комплектации.
</Typography>
<Typography
sx={{
fontSize: isMobile ? "4vw" : "1.5rem",
marginBottom: "2rem",
lineHeight: 1.6,
fontFamily: "Unbounded",
}}
>
Также у нас есть проверенные б/у автомобили с прозрачной историей и
готовые варианты из наличия вы можете уехать на новом авто уже в
день покупки.
</Typography>
<Typography
sx={{
fontSize: isMobile ? "4vw" : "1.5rem",
lineHeight: 1.6,
fontFamily: "Unbounded",
}}
>
Наш автосалон работает для тех, кто ценит надежность, выгодные условия
и индивидуальный подход. Оставьте заявку, и мы подберем для вас
идеальный вариант!
</Typography>
</Box>
</Box>
);
}
export default AboutUsPage;

550
src/pages/CarsPage.tsx Normal file
View File

@ -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<Car[]>([]);
const [loading, setLoading] = useState(true);
const [feedbackOpen, setFeedbackOpen] = useState(false);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const scrollTrackRef = useRef<HTMLDivElement>(null);
const scrollThumbRef = useRef<HTMLDivElement>(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 (
<Box
id="available"
sx={{
bgcolor: "white",
color: "black",
padding: isMobile ? "10vw 5vw" : "5vw",
scrollMarginTop: isMobile ? "15vw" : "10vw",
}}
>
<Box
sx={{
maxWidth: "75vw",
margin: "0 auto",
textAlign: "left",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Typography
component="h2"
sx={{
fontFamily: "Unbounded",
fontSize: isMobile ? "8vw" : "4vw",
fontWeight: "bold",
marginBottom: "2vw",
}}
>
АВТОМОБИЛИ <span style={{ color: "#C27664" }}>В НАЛИЧИИ</span>
</Typography>
{loading ? (
<Typography>Загрузка...</Typography>
) : (
<>
{/* Контейнер для карточек с горизонтальным скроллом */}
<Box
ref={scrollContainerRef}
sx={{
display: "flex",
gap: "2vw",
overflowX: "auto",
scrollbarWidth: "none",
"&::-webkit-scrollbar": {
display: "none",
},
pb: "1vw",
maxWidth: isMobile ? "81vw" : "53vw",
scrollBehavior: "smooth",
}}
>
{cars.map((car, index) => (
<Box
key={car.id}
sx={{
width: isMobile ? "80vw" : "25vw",
minWidth: isMobile ? "80vw" : "25vw",
marginLeft: index === 0 ? "0.5vw" : 0,
bgcolor: "white",
border: isMobile ? "1px solid #C27664" : "none",
borderRadius: "2vw",
overflow: "hidden",
boxShadow: isMobile
? "none"
: "-0.5vw 0.5vw 2vw rgba(0, 0, 0, 0.25)",
}}
>
<Box
sx={{
width: "100%",
height: isMobile ? "50vw" : "15vw",
backgroundImage: `url(${getImageUrl(car.image)})`,
backgroundSize: "cover",
backgroundPosition: "center",
}}
/>
<Box sx={{ p: "0.5vw 1.5vw 1.5vw 1.5vw" }}>
<Typography
sx={{
fontFamily: "Unbounded",
fontSize: isMobile ? "4vw" : "1.8vw",
fontWeight: "bold",
}}
>
{car.name}
</Typography>
<Typography
sx={{
fontSize: isMobile ? "3.5vw" : "1vw",
color: "#666",
fontFamily: "Unbounded",
}}
>
Год выпуска {car.year}
</Typography>
<Typography
sx={{
fontSize: isMobile ? "3.5vw" : "1vw",
color: "#666",
fontFamily: "Unbounded",
}}
>
Пробег {car.mileage.toLocaleString()} км.
</Typography>
<Box
sx={{
display: "flex",
alignItems: "center",
width: "100%",
height: "0.2vw",
backgroundColor: "rgba(220, 220, 220, 1)",
borderRadius: "1vw",
mt: "0.5vw",
}}
></Box>
<Typography
sx={{
fontFamily: "Unbounded",
fontSize: isMobile ? "4.5vw" : "1.5vw",
fontWeight: "bold",
}}
>
{car.price.toLocaleString()}
</Typography>
</Box>
</Box>
))}
</Box>
<Box
sx={{
display: "flex",
position: "relative",
top: isMobile ? "3.7vw" : "-14vw",
left: "0",
right: "0",
bottom: "0",
justifyContent: "space-between",
alignItems: "center",
gap: isMobile ? "75vw" : "60vw",
}}
>
<KeyboardDoubleArrowLeftIcon
onClick={scrollLeft}
sx={{
fontSize: isMobile ? "10vw" : "5vw",
cursor: "pointer",
color: "#C27664",
transition: "transform 0.2s",
"&:hover": {
transform: "scale(1.2)",
},
"&:active": {
transform: "scale(0.9)",
},
}}
/>
<KeyboardDoubleArrowRightIcon
onClick={scrollRight}
sx={{
fontSize: isMobile ? "10vw" : "5vw",
cursor: "pointer",
color: "#C27664",
transition: "transform 0.2s",
"&:hover": {
transform: "scale(1.2)",
},
"&:active": {
transform: "scale(0.9)",
},
}}
/>
</Box>
{/* Индикатор скролла */}
<Box
ref={scrollTrackRef}
onClick={handleTrackClick}
sx={{
width: "100%",
height: isMobile ? "6vw" : "2vw",
bgcolor: "#C27664",
borderRadius: "3vw",
mb: "1vw",
position: "relative",
overflow: "hidden",
cursor: "pointer",
boxShadow: "-0.5vw 0.5vw 1vw rgba(0, 0, 0, 0.25)",
mt: "-4vw ",
}}
>
<Box
ref={scrollThumbRef}
onMouseDown={handleDragStart}
onTouchStart={handleTouchStart}
sx={{
width: `${thumbWidth}%`,
height: isMobile ? "4vw" : "1vw",
bgcolor: "#fff",
borderRadius: "3vw",
position: "absolute",
left: isMobile ? "1vw" : "0.5vw",
top: isMobile ? "1vw" : "0.5vw",
cursor: "grab",
boxShadow: "-0.5vw 0.5vw 1vw rgba(0, 0, 0, 0.25)",
"&:active": {
cursor: "grabbing",
},
transition: isDragging ? "none" : "left 0.1s ease",
userSelect: "none", // Предотвращает выделение текста
WebkitUserDrag: "none", // Предотвращает перетаскивание в WebKit
WebkitTouchCallout: "none", // Отключает контекстное меню при длительном нажатии
}}
/>
</Box>
{/* Кнопки */}
<Box
sx={{
display: "flex",
justifyContent: "center",
gap: "2vw",
}}
>
<Box
component="a"
href="https://t.me/your_telegram_channel"
target="_blank"
sx={{
bgcolor: "#C27664",
color: "#fff",
borderRadius: isMobile ? "3vw" : "1.5vw",
padding: isMobile ? "3vw 5vw" : "1vw 2vw",
fontFamily: "Unbounded",
fontSize: isMobile ? "3.5vw" : "1.2vw",
fontWeight: "bold",
cursor: "pointer",
textDecoration: "none",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
Наличие в Telegram
</Box>
<Box
onClick={handleOpenFeedback}
sx={{
bgcolor: "#fff",
color: "#C27664",
border: "2px solid #C27664",
borderRadius: isMobile ? "3vw" : "1.5vw",
padding: isMobile ? "3vw 5vw" : "1vw 2vw",
fontFamily: "Unbounded",
fontSize: isMobile ? "3.5vw" : "1.2vw",
fontWeight: "bold",
cursor: "pointer",
}}
>
Оставить заявку
</Box>
</Box>
</>
)}
</Box>
<Feedback
open={feedbackOpen}
onClose={handleCloseFeedback}
handleScrollToAnchor={() => {}}
/>
</Box>
);
}
export default CarsPage;

151
src/pages/DeliveryPage.tsx Normal file
View File

@ -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 (
<Box
id="about-us"
sx={{
bgcolor: "white",
color: "black",
padding: "5vw",
}}
>
<Box
sx={{
maxWidth: "80vw",
margin: "0 auto",
textAlign: "left",
}}
>
<Typography
component="h1"
sx={{
fontFamily: "Unbounded",
fontSize: isMobile ? "10vw" : "4vw",
fontWeight: "bold",
marginBottom: "1vw",
maxWidth: "50vw",
}}
>
Мы доставляем автомобили из{" "}
<span style={{ color: "#C27664" }}>США, Китая и Кореи</span>
</Typography>
<Box sx={{ display: "flex", gap: "2vw" }}>
<img
src={usa}
alt="usa"
style={{ width: "10vw", height: "9vw", pointerEvents: "none" }}
/>
<img
src={china}
alt="china"
style={{ width: "10vw", height: "9vw", pointerEvents: "none" }}
/>
<img
src={korea}
alt="korea"
style={{ width: "10vw", height: "9vw", pointerEvents: "none" }}
/>
</Box>
</Box>
<Box
sx={{
maxWidth: "80vw",
width: "29vw",
height: "23vw",
bgcolor: "transparent",
position: "relative",
top: "-29vw",
left: "56vw",
borderRadius: "3vw",
border: "0.3vw solid #C27664",
}}
>
<Typography
component="h2"
sx={{
fontFamily: "Unbounded",
fontSize: "2vw",
fontWeight: "bold",
color: "#C27664",
mt: "3vw",
ml: "2.5vw",
maxWidth: "80%",
}}
>
Не знаете какой авто выбрать?
</Typography>
<Typography
component="h2"
sx={{
fontFamily: "Unbounded",
fontSize: "1.5vw",
fontWeight: "light",
color: "#C27664",
ml: "2.5vw",
maxWidth: "60%",
}}
>
Подберите авто прямо сейчас!
</Typography>
<Box
onClick={handleOpenFeedback}
sx={{
fontFamily: "Unbounded",
fontSize: "1.5vw",
color: "white",
bgcolor: "#C27664",
borderRadius: "3vw",
maxWidth: "60%",
cursor: "pointer",
left: "50%",
top: "70%",
position: "absolute",
transform: "translateX(-50%)",
padding: "1vw 3vw 1vw 3vw",
width: "18vw",
textAlign: "center",
"&:hover": {
bgcolor: "#945B4D",
transform: "translateX(-50%) translateY(-5%)",
},
transition: "all 0.3s ease",
}}
>
Подобрать авто
</Box>
</Box>
<Feedback
open={feedbackOpen}
onClose={handleCloseFeedback}
handleScrollToAnchor={handleScrollToAnchor}
/>
</Box>
);
}
export default DeliveryPage;

View File

@ -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,7 +30,15 @@ function MainPage() {
};
return (
<Box sx={{ bgcolor: "#2D2D2D", color: "white", userSelect: "none" }} id="main">
<Box sx={{ bgcolor: "#2D2D2D", color: "white", userSelect: "none" }}>
<Box
id="main"
sx={{
bgcolor: "transparent",
height: "100%",
width: "100%",
}}
>
<Box
id="title"
sx={{
@ -129,14 +141,20 @@ function MainPage() {
fontWeight: "bold",
fontFamily: "Unbounded",
borderColor: "#C27664",
"&:hover": { borderColor: "#945B4D", bgcolor: "transparent", color: "#945B4D" },
"&:hover": {
borderColor: "#945B4D",
bgcolor: "transparent",
color: "#945B4D",
},
transition: "all 0.3s ease",
}}
>
Подобрать авто
</Button>
</Box>
<Box id="contacts" sx={{
<Box
id="contacts"
sx={{
// position: "absolute",
// top: isMobile ? "18.3vw" : "13.3vw",
// right: "4.5vw",
@ -152,14 +170,15 @@ function MainPage() {
flexDirection: "column",
gap: "1vw",
zIndex: 1,
}}>
}}
>
<IconButton
sx={{
color: "#C27664",
borderRadius: "50%",
width: "3vw",
height: "3vw",
"&:hover": { color: "#945B4D", borderColor: "#945B4D" }
"&:hover": { color: "#945B4D", borderColor: "#945B4D" },
}}
>
<VkIcon sx={{ fontSize: "3vw" }} />
@ -171,7 +190,7 @@ function MainPage() {
borderRadius: "50%",
width: "3vw",
height: "3vw",
"&:hover": { color: "#945B4D", borderColor: "#945B4D" }
"&:hover": { color: "#945B4D", borderColor: "#945B4D" },
}}
>
<TelegramIcon sx={{ fontSize: "3vw" }} />
@ -183,7 +202,7 @@ function MainPage() {
borderRadius: "50%",
width: "3vw",
height: "3vw",
"&:hover": { color: "#945B4D", borderColor: "#945B4D" }
"&:hover": { color: "#945B4D", borderColor: "#945B4D" },
}}
>
<WhatsAppIcon sx={{ fontSize: "3vw" }} />
@ -191,7 +210,16 @@ function MainPage() {
</Box>
</Box>
<Feedback open={feedbackOpen} onClose={handleCloseFeedback} handleScrollToAnchor={handleScrollToAnchor} />
<Feedback
open={feedbackOpen}
onClose={handleCloseFeedback}
handleScrollToAnchor={handleScrollToAnchor}
/>
</Box>
<AboutUsPage />
<StagesPage />
<CarsPage />
<DeliveryPage />
</Box>
);
}

449
src/pages/StagesPage.tsx Normal file
View File

@ -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 (
<Box
id="stages"
sx={{
bgcolor: "white",
color: "black",
padding: isMobile ? "10vw 5vw" : "5vw",
minHeight: "100vh",
scrollMarginTop: isMobile ? "15vw" : "10vw",
position: "relative", // Добавляем позиционирование для родителя
}}
>
<Box
sx={{
maxWidth: "75vw",
margin: "0 auto",
textAlign: "left",
}}
>
<Typography
component="h2"
id="stages"
sx={{
fontFamily: "Unbounded",
fontSize: isMobile ? "8vw" : "4vw",
fontWeight: "bold",
marginBottom: "5vw",
}}
>
ЭТАПЫ <span style={{ color: "#C27664" }}>РАБОТЫ</span>
</Typography>
{isMobile ? (
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "3vw",
alignItems: "center",
}}
>
{stageCards.map((card, index) => (
<Box
key={index}
sx={{
backgroundColor: "#ffffff",
borderRadius: "3vw",
padding: "3vw",
color: "#000000",
boxShadow: "0px 4px 20px rgba(0, 0, 0, 0.1)",
display: "flex",
flexDirection: "column",
width: "100%",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
marginBottom: "2vw",
gap: "1vw",
}}
>
<img
src={card.emoji}
alt={`Этап ${card.number}`}
style={{
width: isMobile ? "10vw" : "5vw",
height: "auto",
marginRight: "1vw",
}}
/>
<Typography
sx={{
fontFamily: "Unbounded",
fontSize: isMobile ? "4vw" : "1.8vw",
fontWeight: "bold",
lineHeight: 1.2,
}}
>
{card.number}. {card.title}
</Typography>
</Box>
<Box>
{card.items.map((item, idx) => (
<Box
key={idx}
sx={{
display: "flex",
alignItems: "flex-start",
marginBottom: "1vw",
}}
>
<Typography
sx={{
fontSize: isMobile ? "3.5vw" : "1.5vw",
lineHeight: 1.3,
fontFamily: "Unbounded",
}}
>
{item}
</Typography>
</Box>
))}
</Box>
</Box>
))}
<Box
onClick={(e) => {
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",
},
},
}}
>
<Box sx={{ padding: "2vw" }}>
<Typography
sx={{
fontFamily: "Unbounded",
fontSize: isMobile ? "6vw" : "2vw",
fontWeight: "bold",
mt: "2vw",
ml: "2vw",
}}
>
Есть вопросы?
</Typography>
<Typography
sx={{
fontSize: isMobile ? "5.2vw" : "1.2vw",
marginBottom: "2vw",
fontFamily: "Unbounded",
ml: "2vw",
}}
>
Оставьте заявку прямо сейчас
</Typography>
<Box
sx={{
width: "100%",
height: isMobile ? "5vw" : "2vw",
backgroundColor: "rgba(255, 255, 255, 1)",
borderRadius: isMobile ? "5vw" : "1vw",
position: "relative",
mt: isMobile ? "46vw" : "15vw",
}}
>
<Box
className="slider-circle"
sx={{
width: isMobile ? "4vw" : "1.5vw",
height: isMobile ? "4vw" : "1.5vw",
backgroundColor: "#9A5B4C",
borderRadius: "50%",
position: "absolute",
left: isMobile ? "0.5vw" : "0.25vw",
top: isMobile ? "0.5vw" : "0.25vw",
transition: "all 0.3s ease",
}}
/>
</Box>
</Box>
</Box>
</Box>
) : (
<Box
sx={{
display: "flex",
flexWrap: "wrap",
gap: "2vw",
justifyContent: "center",
maxWidth: "75vw",
}}
>
{stageCards.map((card, index) => (
<Box
key={index}
sx={{
backgroundColor: "white",
borderRadius: "1.5vw",
padding: "2vw",
color: "#000000",
boxShadow: "-0.5vw 0.5vw 2vw rgba(0, 0, 0, 0.3)",
display: "flex",
flexDirection: "column",
width: "20vw",
marginBottom: "2vw",
height: "25vw",
maxWidth: "20vw",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
marginBottom: "1.5vw",
gap: "1vw",
flexDirection: "column",
textAlign: "center",
}}
>
<img
src={card.emoji}
alt={`Этап ${card.number}`}
style={{
width: "3vw",
height: "auto",
}}
/>
<Typography
sx={{
fontFamily: "Unbounded",
fontSize: "1.2vw",
fontWeight: "bold",
lineHeight: 1.2,
}}
>
{card.number}. {card.title}
</Typography>
</Box>
<Box>
{card.items.map((item, idx) => (
<Box
key={idx}
sx={{
display: "flex",
alignItems: "flex-start",
marginBottom: "0.8vw",
}}
>
<Typography
sx={{
fontSize: "1vw",
lineHeight: 1.3,
fontFamily: "Unbounded",
}}
>
{item}
</Typography>
</Box>
))}
</Box>
</Box>
))}
<Box
onClick={(e) => {
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",
},
},
}}
>
<Box sx={{ padding: "2vw" }}>
<Typography
sx={{
fontFamily: "Unbounded",
fontSize: "2vw",
fontWeight: "bold",
marginBottom: "1.5vw",
}}
>
Есть вопросы?
</Typography>
<Typography
sx={{
fontSize: "1.2vw",
marginBottom: "2vw",
fontFamily: "Unbounded",
}}
>
Оставьте заявку прямо сейчас
</Typography>
<Box
sx={{
width: "100%",
height: "2vw",
backgroundColor: "rgba(255, 255, 255, 1)",
borderRadius: "1vw",
position: "relative",
mt: "15vw",
}}
>
<Box
className="slider-circle"
sx={{
width: "1.5vw",
height: "1.5vw",
backgroundColor: "#9A5B4C",
borderRadius: "50%",
position: "absolute",
left: "0.25vw",
top: "0.25vw",
transition: "all 0.3s ease",
}}
/>
</Box>
</Box>
</Box>
</Box>
)}
</Box>
<Feedback
open={feedbackOpen}
onClose={handleCloseFeedback}
handleScrollToAnchor={() => {}} // Если нужен пустой обработчик для совместимости
/>
</Box>
);
}
export default StagesPage;

131
src/utils/api.ts Normal file
View File

@ -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<CarsResponse> => {
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<CarResponse> => {
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<Car, 'id'>, image?: File): Promise<CarResponse> => {
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<Omit<Car, 'id'>>,
image?: File
): Promise<CarResponse> => {
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<void> => {
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}`;
};

View File

@ -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);
}
};
/**
* Функция для плавного скролла с анимацией
* @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);
}