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, InputLabel,
OutlinedInput, OutlinedInput,
Checkbox, Checkbox,
Dialog,
DialogContent,
} from "@mui/material"; } from "@mui/material";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
@ -53,7 +55,11 @@ interface FeedbackProps {
handleScrollToAnchor?: (anchor: string) => void; 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 [name, setName] = useState("");
const [phone, setPhone] = useState("+7"); const [phone, setPhone] = useState("+7");
const [country, setCountry] = useState("Европа"); const [country, setCountry] = useState("Европа");
@ -64,9 +70,7 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
React.useEffect(() => { React.useEffect(() => {
if (open) { if (open) {
window.history.pushState(null, '', '#feedback'); window.history.pushState(null, "", "#feedback");
} else if (!open && !isMobile && handleScrollToAnchor) {
handleScrollToAnchor('#main');
} }
}, [open, handleScrollToAnchor]); }, [open, handleScrollToAnchor]);
@ -82,7 +86,7 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
onClose(); onClose();
if (handleScrollToAnchor) { if (handleScrollToAnchor) {
handleScrollToAnchor('#main'); handleScrollToAnchor("#main");
} }
}; };
@ -98,15 +102,17 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
"& .MuiOutlinedInput-root, fieldset": { "& .MuiOutlinedInput-root, fieldset": {
borderRadius: isMobile ? "3vw" : "1.5vw", borderRadius: isMobile ? "3vw" : "1.5vw",
fontSize: isMobile ? "2.8vw" : "1.25vw", fontSize: isMobile ? "2.8vw" : "1.25vw",
fontFamily: "Unbounded",
}, },
"& .MuiInputLabel-root": { "& .MuiInputLabel-root": {
fontSize: isMobile ? "2.8vw" : "1.25vw", fontSize: isMobile ? "2.8vw" : "1.25vw",
fontFamily: "Unbounded",
transform: isMobile transform: isMobile
? "translate(2.5vw, 3.1vw) scale(1)" ? "translate(2.5vw, 2.1vw) scale(1)"
: "translate(1vw, 1.1vw) scale(1)", : "translate(1vw, 1.1vw) scale(1)",
"&.MuiInputLabel-shrink": { "&.MuiInputLabel-shrink": {
transform: isMobile 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)", : "translate(0.9vw, -0.8vw) scale(0.75)",
}, },
"&.Mui-focused": { "&.Mui-focused": {
@ -114,59 +120,45 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
}, },
}, },
"& .MuiInputBase-input": { "& .MuiInputBase-input": {
fontSize: isMobile ? "2.8vw" : "1.25vw", fontSize: isMobile ? "2.5vw" : "1.25vw",
padding: isMobile ? "3vw 2.5vw" : "1vw 1.5vw", padding: isMobile ? "2vw 2.5vw" : "1vw 1.5vw",
}, },
}; };
const handlePhoneChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handlePhoneChange = (event: React.ChangeEvent<HTMLInputElement>) => {
// Убедимся, что +7 всегда присутствует
if (event.target.value.startsWith("+7")) { if (event.target.value.startsWith("+7")) {
setPhone(event.target.value); setPhone(event.target.value);
} }
}; };
return ( return (
<Box <Dialog
sx={{ open={open}
position: "absolute", onClose={onClose}
top: 0, maxWidth="md"
left: 0, fullWidth
width: "100%", PaperProps={{
height: "100%", sx: {
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",
borderRadius: isMobile ? "3vw" : "1.5vw", borderRadius: isMobile ? "3vw" : "1.5vw",
p: isMobile ? "6vw" : "4vw", margin: "auto",
maxWidth: isMobile ? "70vw" : "40vw", maxWidth: isMobile ? "85vw" : "38vw",
maxHeight: "75vh", position: "fixed",
overflow: "hidden", top: "50%",
boxShadow: 24, // тень как у диалога left: "50%",
margin: "auto", // для дополнительной центровки transform: "translate(-50%, -50%)",
overflowY: "hidden",
},
}}
>
<DialogContent
sx={{
padding: isMobile ? "4vw" : "3vw",
overflowY: "hidden",
}} }}
onClick={(e) => e.stopPropagation()} // предотвращаем закрытие при клике на контент
> >
<Box> <Box>
<IconButton <IconButton
onClick={() => { onClick={onClose}
onClose();
if (handleScrollToAnchor) handleScrollToAnchor('#main');
}}
sx={{ sx={{
position: "absolute", position: "absolute",
right: "1vw", right: "1vw",
@ -181,20 +173,24 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
<Typography <Typography
variant="h5" variant="h5"
fontWeight="bold" fontWeight="bold"
sx={{ fontSize: isMobile ? "5vw" : "1.9vw" }} sx={{
fontSize: isMobile ? "5vw" : "1.9vw",
fontFamily: "Unbounded",
}}
> >
Оставьте заявку Оставьте заявку
</Typography> </Typography>
</Box> </Box>
<Box sx={{}}> <Box>
<Typography <Typography
variant="body1" variant="body1"
sx={{ sx={{
textAlign: "center", textAlign: "center",
fontSize: isMobile ? "3.5vw" : "1.25vw", fontSize: isMobile ? "3.5vw" : "1.25vw",
maxWidth: "65vw", maxWidth: "100%",
margin: "auto", margin: "auto",
fontFamily: "Unbounded",
}} }}
> >
И наш менеджер свяжется с вами для уточнения деталей заказа И наш менеджер свяжется с вами для уточнения деталей заказа
@ -216,7 +212,6 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
}} }}
/> />
{/* Поле с телефоном как на скриншоте */}
<FormControl <FormControl
fullWidth fullWidth
variant="outlined" variant="outlined"
@ -244,7 +239,11 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
id="phone-input" id="phone-input"
value={phone} value={phone}
onChange={handlePhoneChange} 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="Ваш телефон*" label="Ваш телефон*"
notched notched
sx={{ sx={{
@ -256,10 +255,13 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
/> />
</FormControl> </FormControl>
<Box sx={{ mb: isMobile ? "3vw" : "0.5vw" }}> <Box sx={{ mb: isMobile ? "2vw" : "0.5vw" }}>
<Typography <Typography
variant="body2" variant="body2"
sx={{ mb: "0.5vw", fontSize: isMobile ? "3.5vw" : "1.1vw" }} sx={{
fontSize: isMobile ? "3vw" : "1vw",
fontFamily: "Unbounded",
}}
> >
Из какой страны привезти автомобиль? Из какой страны привезти автомобиль?
</Typography> </Typography>
@ -285,7 +287,10 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
} }
label={ label={
<Typography <Typography
sx={{ fontSize: isMobile ? "3vw" : "1.1vw" }} sx={{
fontSize: isMobile ? "3vw" : "1.1vw",
fontFamily: "Unbounded",
}}
> >
Европа Европа
</Typography> </Typography>
@ -311,7 +316,10 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
} }
label={ label={
<Typography <Typography
sx={{ fontSize: isMobile ? "3vw" : "1.1vw" }} sx={{
fontSize: isMobile ? "3vw" : "1.1vw",
fontFamily: "Unbounded",
}}
> >
США США
</Typography> </Typography>
@ -337,7 +345,10 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
} }
label={ label={
<Typography <Typography
sx={{ fontSize: isMobile ? "3vw" : "1.1vw" }} sx={{
fontSize: isMobile ? "3vw" : "1.1vw",
fontFamily: "Unbounded",
}}
> >
Китай Китай
</Typography> </Typography>
@ -363,7 +374,10 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
} }
label={ label={
<Typography <Typography
sx={{ fontSize: isMobile ? "3vw" : "1.1vw" }} sx={{
fontSize: isMobile ? "3vw" : "1.1vw",
fontFamily: "Unbounded",
}}
> >
Корея Корея
</Typography> </Typography>
@ -380,7 +394,11 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
<Box sx={{ mb: isMobile ? "2.5vw" : "1vw" }}> <Box sx={{ mb: isMobile ? "2.5vw" : "1vw" }}>
<Typography <Typography
variant="body2" variant="body2"
sx={{ mb: "0.5vw", fontSize: isMobile ? "3.5vw" : "1.1vw" }} sx={{
mb: "0.5vw",
fontSize: isMobile ? "3vw" : "1.1vw",
fontFamily: "Unbounded",
}}
> >
Какой у вас бюджет на автомобиль? Какой у вас бюджет на автомобиль?
</Typography> </Typography>
@ -406,7 +424,10 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
} }
label={ label={
<Typography <Typography
sx={{ fontSize: isMobile ? "3vw" : "1.1vw" }} sx={{
fontSize: isMobile ? "3vw" : "1.1vw",
fontFamily: "Unbounded",
}}
> >
до 3 млн до 3 млн
</Typography> </Typography>
@ -417,7 +438,7 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
}} }}
/> />
<FormControlLabel <FormControlLabel
value="до 5 млн" value="3-5 млн"
control={ control={
<Radio <Radio
sx={{ sx={{
@ -432,7 +453,10 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
} }
label={ label={
<Typography <Typography
sx={{ fontSize: isMobile ? "3vw" : "1.1vw" }} sx={{
fontSize: isMobile ? "3vw" : "1.1vw",
fontFamily: "Unbounded",
}}
> >
3-5 млн 3-5 млн
</Typography> </Typography>
@ -458,7 +482,10 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
} }
label={ label={
<Typography <Typography
sx={{ fontSize: isMobile ? "3vw" : "1.1vw" }} sx={{
fontSize: isMobile ? "3vw" : "1.1vw",
fontFamily: "Unbounded",
}}
> >
5-10 млн 5-10 млн
</Typography> </Typography>
@ -469,7 +496,7 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
}} }}
/> />
<FormControlLabel <FormControlLabel
value="более 10 млн" value="10+ млн"
control={ control={
<Radio <Radio
sx={{ sx={{
@ -484,7 +511,10 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
} }
label={ label={
<Typography <Typography
sx={{ fontSize: isMobile ? "3vw" : "1.1vw" }} sx={{
fontSize: isMobile ? "3vw" : "1.1vw",
fontFamily: "Unbounded",
}}
> >
10+ млн 10+ млн
</Typography> </Typography>
@ -524,6 +554,7 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
py: "0.9vw", py: "0.9vw",
borderRadius: isMobile ? "3vw" : "1vw", borderRadius: isMobile ? "3vw" : "1vw",
fontSize: isMobile ? "3.5vw" : "1.25vw", fontSize: isMobile ? "3.5vw" : "1.25vw",
fontFamily: "Unbounded",
"&:hover": { bgcolor: "#a42517" }, "&:hover": { bgcolor: "#a42517" },
textTransform: "none", textTransform: "none",
}} }}
@ -538,10 +569,7 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
<Box <Box
sx={{ sx={{
mt: isMobile ? "2vw" : "1vw", mt: isMobile ? "1vw" : "0.5vw",
display: "flex",
alignItems: "center",
justifyContent: "center",
}} }}
> >
<FormControlLabel <FormControlLabel
@ -566,7 +594,10 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
label={ label={
<Typography <Typography
variant="body2" variant="body2"
sx={{ fontSize: isMobile ? "2.5vw" : "1vw" }} sx={{
fontSize: isMobile ? "2.5vw" : "1vw",
fontFamily: "Unbounded",
}}
> >
Я подтверждаю, что ознакомлен{" "} Я подтверждаю, что ознакомлен{" "}
<Typography <Typography
@ -576,6 +607,7 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
cursor: "pointer", cursor: "pointer",
textDecoration: "underline", textDecoration: "underline",
fontSize: isMobile ? "2.5vw" : "1vw", fontSize: isMobile ? "2.5vw" : "1vw",
fontFamily: "Unbounded",
}} }}
> >
с политикой конфиденциальности с политикой конфиденциальности
@ -587,8 +619,8 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
</Box> </Box>
</Box> </Box>
</Box> </Box>
</Box> </DialogContent>
</Box> </Dialog>
); );
}; };

View File

@ -21,7 +21,7 @@ const Header = () => {
const [scrolled, setScrolled] = useState(false); const [scrolled, setScrolled] = useState(false);
const menuItems = [ const menuItems = [
{ title: "О нас", anchor: "#about" }, { title: "О нас", anchor: "#about-us" },
{ title: "Калькулятор", anchor: "#calculator" }, { title: "Калькулятор", anchor: "#calculator" },
{ title: "Отзывы", anchor: "#reviews" }, { title: "Отзывы", anchor: "#reviews" },
{ title: "Контакты", anchor: "#contacts" }, { title: "Контакты", anchor: "#contacts" },
@ -29,7 +29,7 @@ const Header = () => {
{ title: "Команда", anchor: "#team" }, { title: "Команда", anchor: "#team" },
{ title: "Доставленные авто", anchor: "#delivered" }, { title: "Доставленные авто", anchor: "#delivered" },
{ title: "Этапы работы", anchor: "#stages" }, { title: "Этапы работы", anchor: "#stages" },
{ title: " ", anchor: "#main"}, { title: " ", anchor: "#main" },
]; ];
// Отслеживание скролла // Отслеживание скролла
@ -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 ( if (
event.type === "keydown" && 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; return;
} }
@ -98,8 +100,7 @@ const Header = () => {
"&:hover": { scale: 1.1 }, "&:hover": { scale: 1.1 },
transition: "all 0.4s ease", transition: "all 0.4s ease",
}} }}
> ></Box>
</Box>
{isMobile ? ( {isMobile ? (
<Box <Box
@ -154,11 +155,7 @@ const Header = () => {
<Box sx={{ height: isMobile ? "15vw" : "10vw" }} /> <Box sx={{ height: isMobile ? "15vw" : "10vw" }} />
{/* Мобильное меню */} {/* Мобильное меню */}
<Drawer <Drawer anchor="right" open={drawerOpen} onClose={toggleDrawer(false)}>
anchor="right"
open={drawerOpen}
onClose={toggleDrawer(false)}
>
<Box <Box
sx={{ width: "58vw", bgcolor: "#2D2D2D", height: "100%" }} sx={{ width: "58vw", bgcolor: "#2D2D2D", height: "100%" }}
role="presentation" role="presentation"
@ -188,7 +185,11 @@ const Header = () => {
</Box> </Box>
</Drawer> </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 Feedback from "../components/Feedback";
import car from "../../src/assets/icon/car.png"; import car from "../../src/assets/icon/car.png";
import { useResponsive } from "../theme/useResponsive"; import { useResponsive } from "../theme/useResponsive";
import TelegramIcon from '@mui/icons-material/Telegram'; import TelegramIcon from "@mui/icons-material/Telegram";
import VkIcon from '@mui/icons-material/Facebook'; import VkIcon from "@mui/icons-material/Facebook";
import WhatsAppIcon from '@mui/icons-material/WhatsApp'; import WhatsAppIcon from "@mui/icons-material/WhatsApp";
import { scrollToAnchor } from "../utils/scrollUtils"; import { scrollToAnchor } from "../utils/scrollUtils";
import AboutUsPage from "./AboutUsPage";
import StagesPage from "./StagesPage";
import CarsPage from "./CarsPage";
import DeliveryPage from "./DeliveryPage";
function MainPage() { function MainPage() {
const [feedbackOpen, setFeedbackOpen] = useState(false); const [feedbackOpen, setFeedbackOpen] = useState(false);
@ -26,7 +30,15 @@ function MainPage() {
}; };
return ( 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 <Box
id="title" id="title"
sx={{ sx={{
@ -129,14 +141,20 @@ function MainPage() {
fontWeight: "bold", fontWeight: "bold",
fontFamily: "Unbounded", fontFamily: "Unbounded",
borderColor: "#C27664", borderColor: "#C27664",
"&:hover": { borderColor: "#945B4D", bgcolor: "transparent", color: "#945B4D" }, "&:hover": {
borderColor: "#945B4D",
bgcolor: "transparent",
color: "#945B4D",
},
transition: "all 0.3s ease", transition: "all 0.3s ease",
}} }}
> >
Подобрать авто Подобрать авто
</Button> </Button>
</Box> </Box>
<Box id="contacts" sx={{ <Box
id="contacts"
sx={{
// position: "absolute", // position: "absolute",
// top: isMobile ? "18.3vw" : "13.3vw", // top: isMobile ? "18.3vw" : "13.3vw",
// right: "4.5vw", // right: "4.5vw",
@ -152,14 +170,15 @@ function MainPage() {
flexDirection: "column", flexDirection: "column",
gap: "1vw", gap: "1vw",
zIndex: 1, zIndex: 1,
}}> }}
>
<IconButton <IconButton
sx={{ sx={{
color: "#C27664", color: "#C27664",
borderRadius: "50%", borderRadius: "50%",
width: "3vw", width: "3vw",
height: "3vw", height: "3vw",
"&:hover": { color: "#945B4D", borderColor: "#945B4D" } "&:hover": { color: "#945B4D", borderColor: "#945B4D" },
}} }}
> >
<VkIcon sx={{ fontSize: "3vw" }} /> <VkIcon sx={{ fontSize: "3vw" }} />
@ -171,7 +190,7 @@ function MainPage() {
borderRadius: "50%", borderRadius: "50%",
width: "3vw", width: "3vw",
height: "3vw", height: "3vw",
"&:hover": { color: "#945B4D", borderColor: "#945B4D" } "&:hover": { color: "#945B4D", borderColor: "#945B4D" },
}} }}
> >
<TelegramIcon sx={{ fontSize: "3vw" }} /> <TelegramIcon sx={{ fontSize: "3vw" }} />
@ -183,7 +202,7 @@ function MainPage() {
borderRadius: "50%", borderRadius: "50%",
width: "3vw", width: "3vw",
height: "3vw", height: "3vw",
"&:hover": { color: "#945B4D", borderColor: "#945B4D" } "&:hover": { color: "#945B4D", borderColor: "#945B4D" },
}} }}
> >
<WhatsAppIcon sx={{ fontSize: "3vw" }} /> <WhatsAppIcon sx={{ fontSize: "3vw" }} />
@ -191,7 +210,16 @@ function MainPage() {
</Box> </Box>
</Box> </Box>
<Feedback open={feedbackOpen} onClose={handleCloseFeedback} handleScrollToAnchor={handleScrollToAnchor} /> <Feedback
open={feedbackOpen}
onClose={handleCloseFeedback}
handleScrollToAnchor={handleScrollToAnchor}
/>
</Box>
<AboutUsPage />
<StagesPage />
<CarsPage />
<DeliveryPage />
</Box> </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 anchor - идентификатор якоря, например "#about"
* @param setDrawerOpen - опциональная функция для закрытия мобильного меню * @param setDrawerOpen - опциональная функция для закрытия мобильного меню
* @param isMobile - флаг мобильного устройства * @param isMobile - флаг мобильного устройства
@ -11,11 +11,53 @@ export const scrollToAnchor = (
): void => { ): void => {
const element = document.querySelector(anchor); const element = document.querySelector(anchor);
if (element) { 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 с хешем без перезагрузки страницы // Обновляем URL с хешем без перезагрузки страницы
window.history.pushState(null, '', anchor); window.history.pushState(null, '', anchor);
// Реализация плавного скролла с анимацией
smoothScrollTo(targetPosition);
} }
if (isMobile && setDrawerOpen) { if (isMobile && setDrawerOpen) {
setDrawerOpen(false); 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);
}