add new pages
BIN
src/assets/emoji/1.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
src/assets/emoji/2.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/emoji/3.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
src/assets/emoji/4.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
src/assets/emoji/5.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
src/assets/emoji/china.png
Normal file
After Width: | Height: | Size: 8.8 KiB |
BIN
src/assets/emoji/korea.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/emoji/united-states.png
Normal file
After Width: | Height: | Size: 24 KiB |
@ -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]);
|
||||
|
||||
@ -79,10 +83,10 @@ const Feedback: React.FC<FeedbackProps> = ({ open, onClose, handleScrollToAnchor
|
||||
return;
|
||||
}
|
||||
console.log({ name, phone, country, budget, description, agreeToPolicy });
|
||||
|
||||
|
||||
onClose();
|
||||
if (handleScrollToAnchor) {
|
||||
handleScrollToAnchor('#main');
|
||||
handleScrollToAnchor("#main");
|
||||
}
|
||||
};
|
||||
|
||||
@ -98,15 +102,17 @@ const Feedback: React.FC<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');
|
||||
}}
|
||||
<IconButton
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -21,7 +21,7 @@ const Header = () => {
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
|
||||
const menuItems = [
|
||||
{ title: "О нас", anchor: "#about" },
|
||||
{ title: "О нас", anchor: "#about-us" },
|
||||
{ title: "Калькулятор", anchor: "#calculator" },
|
||||
{ title: "Отзывы", anchor: "#reviews" },
|
||||
{ title: "Контакты", anchor: "#contacts" },
|
||||
@ -29,7 +29,7 @@ const Header = () => {
|
||||
{ title: "Команда", anchor: "#team" },
|
||||
{ title: "Доставленные авто", anchor: "#delivered" },
|
||||
{ title: "Этапы работы", anchor: "#stages" },
|
||||
{ title: " ", anchor: "#main"},
|
||||
{ title: " ", anchor: "#main" },
|
||||
];
|
||||
|
||||
// Отслеживание скролла
|
||||
@ -48,21 +48,23 @@ const Header = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const toggleDrawer = (open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => {
|
||||
if (
|
||||
event.type === "keydown" &&
|
||||
((event as React.KeyboardEvent).key === "Tab" || (event as React.KeyboardEvent).key === "Shift")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setDrawerOpen(open);
|
||||
};
|
||||
|
||||
// Используем глобальную функцию из utils
|
||||
const handleScrollToAnchor = (anchor: string) => {
|
||||
scrollToAnchor(anchor, setDrawerOpen, isMobile);
|
||||
const toggleDrawer =
|
||||
(open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => {
|
||||
if (
|
||||
event.type === "keydown" &&
|
||||
((event as React.KeyboardEvent).key === "Tab" ||
|
||||
(event as React.KeyboardEvent).key === "Shift")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setDrawerOpen(open);
|
||||
};
|
||||
|
||||
// Используем глобальную функцию из utils
|
||||
const handleScrollToAnchor = (anchor: string) => {
|
||||
scrollToAnchor(anchor, setDrawerOpen, isMobile);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
@ -98,8 +100,7 @@ const Header = () => {
|
||||
"&:hover": { scale: 1.1 },
|
||||
transition: "all 0.4s ease",
|
||||
}}
|
||||
>
|
||||
</Box>
|
||||
></Box>
|
||||
|
||||
{isMobile ? (
|
||||
<Box
|
||||
@ -154,19 +155,15 @@ 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"
|
||||
>
|
||||
<List>
|
||||
{menuItems.map((item, index) => (
|
||||
<ListItem
|
||||
key={index}
|
||||
<ListItem
|
||||
key={index}
|
||||
onClick={() => handleScrollToAnchor(item.anchor)}
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
@ -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
@ -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
@ -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
@ -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;
|
@ -3,10 +3,14 @@ import { Button, Box, Typography, IconButton } from "@mui/material";
|
||||
import Feedback from "../components/Feedback";
|
||||
import car from "../../src/assets/icon/car.png";
|
||||
import { useResponsive } from "../theme/useResponsive";
|
||||
import TelegramIcon from '@mui/icons-material/Telegram';
|
||||
import VkIcon from '@mui/icons-material/Facebook';
|
||||
import WhatsAppIcon from '@mui/icons-material/WhatsApp';
|
||||
import TelegramIcon from "@mui/icons-material/Telegram";
|
||||
import VkIcon from "@mui/icons-material/Facebook";
|
||||
import WhatsAppIcon from "@mui/icons-material/WhatsApp";
|
||||
import { scrollToAnchor } from "../utils/scrollUtils";
|
||||
import AboutUsPage from "./AboutUsPage";
|
||||
import StagesPage from "./StagesPage";
|
||||
import CarsPage from "./CarsPage";
|
||||
import DeliveryPage from "./DeliveryPage";
|
||||
|
||||
function MainPage() {
|
||||
const [feedbackOpen, setFeedbackOpen] = useState(false);
|
||||
@ -26,172 +30,196 @@ function MainPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ bgcolor: "#2D2D2D", color: "white", userSelect: "none" }} id="main">
|
||||
<Box sx={{ bgcolor: "#2D2D2D", color: "white", userSelect: "none" }}>
|
||||
<Box
|
||||
id="title"
|
||||
id="main"
|
||||
sx={{
|
||||
textAlign: "left",
|
||||
width: "35vw",
|
||||
ml: "5vw",
|
||||
height: "30vw",
|
||||
bgcolor: "transparent",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: "Unbounded",
|
||||
fontSize: "9vw",
|
||||
fontWeight: "bold",
|
||||
position: "relative",
|
||||
zIndex: 1,
|
||||
left: "3vw",
|
||||
background:
|
||||
"linear-gradient(90deg, rgb(255, 255, 255), rgb(105, 105, 105))",
|
||||
WebkitBackgroundClip: "text",
|
||||
WebkitTextFillColor: "transparent",
|
||||
backgroundClip: "text",
|
||||
color: "transparent",
|
||||
}}
|
||||
>
|
||||
АВТО
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: "Unbounded",
|
||||
fontSize: "9vw",
|
||||
fontWeight: "bold",
|
||||
position: "relative",
|
||||
mt: "-5.5vw",
|
||||
zIndex: 1,
|
||||
right: "2vw",
|
||||
background:
|
||||
"linear-gradient(90deg, rgb(255, 255, 255), rgb(0, 0, 0))",
|
||||
WebkitBackgroundClip: "text",
|
||||
WebkitTextFillColor: "transparent",
|
||||
backgroundClip: "text",
|
||||
color: "transparent",
|
||||
}}
|
||||
>
|
||||
БРО
|
||||
</Typography>
|
||||
<img
|
||||
src={car}
|
||||
alt="logo"
|
||||
style={{
|
||||
width: "60vw",
|
||||
height: "32vw",
|
||||
position: "relative",
|
||||
top: "-19vw",
|
||||
left: "12vw",
|
||||
zIndex: 2,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box
|
||||
id="title"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
right: "4.5vw",
|
||||
top: isMobile ? "18.3vw" : "13.3vw",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "1vw",
|
||||
zIndex: 1,
|
||||
textAlign: "left",
|
||||
width: "35vw",
|
||||
ml: "5vw",
|
||||
height: "30vw",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onClick={() => handleScrollToAnchor("#calculator")}
|
||||
variant="contained"
|
||||
<Typography
|
||||
sx={{
|
||||
bgcolor: "#C27664",
|
||||
color: "white",
|
||||
fontSize: "1.5vw",
|
||||
padding: "2.2vw 2vw",
|
||||
textTransform: "none",
|
||||
borderRadius: "3vw",
|
||||
fontWeight: "bold",
|
||||
fontFamily: "Unbounded",
|
||||
"&:hover": { bgcolor: "#945B4D" },
|
||||
transition: "all 0.3s ease",
|
||||
fontSize: "9vw",
|
||||
fontWeight: "bold",
|
||||
position: "relative",
|
||||
zIndex: 1,
|
||||
left: "3vw",
|
||||
background:
|
||||
"linear-gradient(90deg, rgb(255, 255, 255), rgb(105, 105, 105))",
|
||||
WebkitBackgroundClip: "text",
|
||||
WebkitTextFillColor: "transparent",
|
||||
backgroundClip: "text",
|
||||
color: "transparent",
|
||||
}}
|
||||
>
|
||||
Рассчитать стоимость
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpenFeedback}
|
||||
АВТО
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
color: "#C27664",
|
||||
fontSize: "1.5vw",
|
||||
padding: "2.2vw 2vw",
|
||||
textTransform: "none",
|
||||
borderRadius: "3vw",
|
||||
fontWeight: "bold",
|
||||
fontFamily: "Unbounded",
|
||||
borderColor: "#C27664",
|
||||
"&:hover": { borderColor: "#945B4D", bgcolor: "transparent", color: "#945B4D" },
|
||||
transition: "all 0.3s ease",
|
||||
fontSize: "9vw",
|
||||
fontWeight: "bold",
|
||||
position: "relative",
|
||||
mt: "-5.5vw",
|
||||
zIndex: 1,
|
||||
right: "2vw",
|
||||
background:
|
||||
"linear-gradient(90deg, rgb(255, 255, 255), rgb(0, 0, 0))",
|
||||
WebkitBackgroundClip: "text",
|
||||
WebkitTextFillColor: "transparent",
|
||||
backgroundClip: "text",
|
||||
color: "transparent",
|
||||
}}
|
||||
>
|
||||
Подобрать авто
|
||||
</Button>
|
||||
</Box>
|
||||
<Box id="contacts" sx={{
|
||||
// position: "absolute",
|
||||
// top: isMobile ? "18.3vw" : "13.3vw",
|
||||
// right: "4.5vw",
|
||||
// width: "100%",
|
||||
// display: "flex",
|
||||
// flexDirection: "column",
|
||||
// gap: isMobile ? "1vw" : "2vw",
|
||||
БРО
|
||||
</Typography>
|
||||
<img
|
||||
src={car}
|
||||
alt="logo"
|
||||
style={{
|
||||
width: "60vw",
|
||||
height: "32vw",
|
||||
position: "relative",
|
||||
top: "-19vw",
|
||||
left: "12vw",
|
||||
zIndex: 2,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
|
||||
position: "absolute",
|
||||
right: "1vw",
|
||||
top: isMobile ? "20.3vw" : "15.3vw",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "1vw",
|
||||
zIndex: 1,
|
||||
}}>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: "#C27664",
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
right: "4.5vw",
|
||||
top: isMobile ? "18.3vw" : "13.3vw",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "1vw",
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onClick={() => handleScrollToAnchor("#calculator")}
|
||||
variant="contained"
|
||||
sx={{
|
||||
bgcolor: "#C27664",
|
||||
color: "white",
|
||||
fontSize: "1.5vw",
|
||||
padding: "2.2vw 2vw",
|
||||
textTransform: "none",
|
||||
borderRadius: "3vw",
|
||||
fontWeight: "bold",
|
||||
fontFamily: "Unbounded",
|
||||
"&:hover": { bgcolor: "#945B4D" },
|
||||
transition: "all 0.3s ease",
|
||||
}}
|
||||
>
|
||||
Рассчитать стоимость
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpenFeedback}
|
||||
sx={{
|
||||
color: "#C27664",
|
||||
fontSize: "1.5vw",
|
||||
padding: "2.2vw 2vw",
|
||||
textTransform: "none",
|
||||
borderRadius: "3vw",
|
||||
fontWeight: "bold",
|
||||
fontFamily: "Unbounded",
|
||||
borderColor: "#C27664",
|
||||
"&:hover": {
|
||||
borderColor: "#945B4D",
|
||||
bgcolor: "transparent",
|
||||
color: "#945B4D",
|
||||
},
|
||||
transition: "all 0.3s ease",
|
||||
}}
|
||||
>
|
||||
Подобрать авто
|
||||
</Button>
|
||||
</Box>
|
||||
<Box
|
||||
id="contacts"
|
||||
sx={{
|
||||
// position: "absolute",
|
||||
// top: isMobile ? "18.3vw" : "13.3vw",
|
||||
// right: "4.5vw",
|
||||
// width: "100%",
|
||||
// display: "flex",
|
||||
// flexDirection: "column",
|
||||
// gap: isMobile ? "1vw" : "2vw",
|
||||
|
||||
position: "absolute",
|
||||
right: "1vw",
|
||||
top: isMobile ? "20.3vw" : "15.3vw",
|
||||
display: "flex",
|
||||
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" }} />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
sx={{
|
||||
color: "#C27664",
|
||||
|
||||
<IconButton
|
||||
sx={{
|
||||
color: "#C27664",
|
||||
borderRadius: "50%",
|
||||
width: "3vw",
|
||||
height: "3vw",
|
||||
"&:hover": { color: "#945B4D", borderColor: "#945B4D" }
|
||||
"&:hover": { color: "#945B4D", borderColor: "#945B4D" },
|
||||
}}
|
||||
>
|
||||
<TelegramIcon sx={{ fontSize: "3vw" }} />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
sx={{
|
||||
color: "#C27664",
|
||||
|
||||
<IconButton
|
||||
sx={{
|
||||
color: "#C27664",
|
||||
borderRadius: "50%",
|
||||
width: "3vw",
|
||||
height: "3vw",
|
||||
"&:hover": { color: "#945B4D", borderColor: "#945B4D" }
|
||||
"&:hover": { color: "#945B4D", borderColor: "#945B4D" },
|
||||
}}
|
||||
>
|
||||
<WhatsAppIcon sx={{ fontSize: "3vw" }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</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
@ -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
@ -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}`;
|
||||
};
|
@ -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);
|
||||
}
|