add persons, / footer(contacts) / main car page
This commit is contained in:
@ -1,5 +1,6 @@
|
||||
import { Routes, Route } from 'react-router-dom'
|
||||
import MainPage from './pages/MainPage.tsx'
|
||||
import CarPage from './pages/CarPage.tsx'
|
||||
import Header from './components/Header.tsx'
|
||||
|
||||
function App() {
|
||||
@ -15,6 +16,10 @@ function App() {
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/car/:id"
|
||||
element={<CarPage />}
|
||||
/>
|
||||
</Routes>
|
||||
</>
|
||||
)
|
||||
|
@ -2,10 +2,15 @@ import { Box } from "@mui/material";
|
||||
import React from "react";
|
||||
import { useResponsive } from "../theme/useResponsive";
|
||||
|
||||
function Divider() {
|
||||
interface DividerProps {
|
||||
marginTopDivider?: string | "1vw";
|
||||
marginBottomDivider?: string | "1vw";
|
||||
}
|
||||
|
||||
function Divider({ marginTopDivider, marginBottomDivider }: DividerProps) {
|
||||
const { isMobile } = useResponsive();
|
||||
return (
|
||||
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "center", bgcolor: "#fff" }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
@ -14,9 +19,9 @@ function Divider() {
|
||||
height: isMobile ? "0.3vw" : "0.15vw",
|
||||
backgroundColor: "rgba(220, 220, 220, 1)",
|
||||
borderRadius: "1vw",
|
||||
position: "absolute",
|
||||
mt: "1vw",
|
||||
mb: "1vw",
|
||||
// position: "absolute",
|
||||
mt: marginTopDivider,
|
||||
mb: marginBottomDivider,
|
||||
}}
|
||||
></Box>
|
||||
</Box>
|
||||
|
@ -62,7 +62,7 @@ const Feedback: React.FC<FeedbackProps> = ({
|
||||
}) => {
|
||||
const [name, setName] = useState("");
|
||||
const [phone, setPhone] = useState("+7");
|
||||
const [country, setCountry] = useState("Европа");
|
||||
const [country, setCountry] = useState("США");
|
||||
const [budget, setBudget] = useState("до 3 млн");
|
||||
const [description, setDescription] = useState("");
|
||||
const [agreeToPolicy, setAgreeToPolicy] = useState(false);
|
||||
@ -271,35 +271,6 @@ const Feedback: React.FC<FeedbackProps> = ({
|
||||
value={country}
|
||||
onChange={(e) => setCountry(e.target.value)}
|
||||
>
|
||||
<FormControlLabel
|
||||
value="Европа"
|
||||
control={
|
||||
<Radio
|
||||
sx={{
|
||||
color: "#C27664",
|
||||
"&.Mui-checked": { color: "#C27664" },
|
||||
"& .MuiSvgIcon-root": {
|
||||
fontSize: isMobile ? "4.5vw" : "1.5vw",
|
||||
},
|
||||
padding: "0.5vw",
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: isMobile ? "3vw" : "1.1vw",
|
||||
fontFamily: "Unbounded",
|
||||
}}
|
||||
>
|
||||
Европа
|
||||
</Typography>
|
||||
}
|
||||
sx={{
|
||||
marginRight: isMobile ? "2.5vw" : "1.5vw",
|
||||
ml: "0vw",
|
||||
}}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="США"
|
||||
control={
|
||||
|
@ -14,22 +14,25 @@ import logo from "../assets/icon/autobro.png";
|
||||
import { useResponsive } from "../theme/useResponsive";
|
||||
import Feedback from "./Feedback";
|
||||
import { scrollToAnchor } from "../utils/scrollUtils";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const Header = () => {
|
||||
const { isMobile } = useResponsive();
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [feedbackOpen, setFeedbackOpen] = useState(false);
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const menuItems = [
|
||||
// { title: "", anchor: "#main" },
|
||||
{ title: "О нас", anchor: "#about-us" },
|
||||
{ title: "Этапы работы", anchor: "#stages" },
|
||||
{ title: "В наличии", anchor: "#available" },
|
||||
{ title: "Калькулятор", anchor: "#calculator" },
|
||||
{ title: "Команда", anchor: "#team" },
|
||||
{ title: "Отзывы", anchor: "#reviews" },
|
||||
{ title: "Контакты", anchor: "#contacts" },
|
||||
{ title: "В наличии", anchor: "#available" },
|
||||
{ title: "Команда", anchor: "#team" },
|
||||
{ title: "Доставленные авто", anchor: "#delivered" },
|
||||
{ title: "Этапы работы", anchor: "#stages" },
|
||||
{ title: " ", anchor: "#main" },
|
||||
// { title: "Доставленные авто", anchor: "#delivered" },
|
||||
];
|
||||
|
||||
// Отслеживание скролла
|
||||
@ -62,7 +65,11 @@ const Header = () => {
|
||||
|
||||
// Используем глобальную функцию из utils
|
||||
const handleScrollToAnchor = (anchor: string) => {
|
||||
scrollToAnchor(anchor, setDrawerOpen, isMobile);
|
||||
if (location.pathname.startsWith("/car")) {
|
||||
navigate("/");
|
||||
} else {
|
||||
scrollToAnchor(anchor, setDrawerOpen, isMobile);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@ -153,7 +160,7 @@ const Header = () => {
|
||||
</Box>
|
||||
|
||||
{/* Пустой блок для компенсации фиксированного хедера */}
|
||||
<Box sx={{ height: isMobile ? "15vw" : "10vw", bgcolor: "#2D2D2D" }} />
|
||||
<Box sx={{ height: isMobile ? "15vw" : location.pathname.startsWith("/car") ? "5vw" : "10vw", bgcolor: "#2D2D2D" }} />
|
||||
|
||||
{/* Мобильное меню */}
|
||||
<Drawer anchor="right" open={drawerOpen} onClose={toggleDrawer(false)}>
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
Collapse,
|
||||
Paper,
|
||||
Slide,
|
||||
Checkbox,
|
||||
} from "@mui/material";
|
||||
import { useResponsive } from "../theme/useResponsive";
|
||||
import russia from "../assets/emoji/russia.png";
|
||||
@ -330,7 +331,6 @@ function CalculatorPage() {
|
||||
scrollMarginTop: isMobile ? "15vw" : "10vw",
|
||||
maxWidth: "100vw",
|
||||
overflowX: "hidden",
|
||||
pb: "25vw",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
@ -1125,7 +1125,7 @@ function CalculatorPage() {
|
||||
>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Radio
|
||||
<Checkbox
|
||||
checked={enhancedPermeability}
|
||||
onChange={() =>
|
||||
setEnhancedPermeability(!enhancedPermeability)
|
||||
|
373
src/pages/CarPage.tsx
Normal file
373
src/pages/CarPage.tsx
Normal file
@ -0,0 +1,373 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Box, Typography, Button, Grid, CircularProgress, IconButton, Popover } from "@mui/material";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { fetchCarById, getImageUrl, type Car } from "../utils/api";
|
||||
import { useResponsive } from "../theme/useResponsive";
|
||||
import Feedback from "../components/Feedback";
|
||||
import TelegramIcon from "@mui/icons-material/Telegram";
|
||||
import { FaVk } from "react-icons/fa";
|
||||
import WhatsAppIcon from "@mui/icons-material/WhatsApp";
|
||||
import InfoOutlineIcon from '@mui/icons-material/InfoOutline';
|
||||
|
||||
interface CarDetails extends Car {
|
||||
country_of_origin?: string;
|
||||
drive_type?: string;
|
||||
engine_capacity?: number;
|
||||
engine_power?: number;
|
||||
engine_type?: string;
|
||||
electric_motor_power?: number;
|
||||
hybrid_type?: string;
|
||||
power_ratio?: string;
|
||||
base_price?: number;
|
||||
}
|
||||
|
||||
function CarPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { isMobile } = useResponsive();
|
||||
const [car, setCar] = useState<CarDetails | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [feedbackOpen, setFeedbackOpen] = useState(false);
|
||||
|
||||
const translateCountry = (country: string) => {
|
||||
switch (country) {
|
||||
case "Russia":
|
||||
return "Россия";
|
||||
case "China":
|
||||
return "Китай";
|
||||
case "Korea":
|
||||
return "Корея";
|
||||
case "USA":
|
||||
return "США";
|
||||
case "Россия":
|
||||
return "Россия";
|
||||
case "Китай":
|
||||
return "Китай";
|
||||
case "Корея":
|
||||
return "Корея";
|
||||
case "США":
|
||||
return "США";
|
||||
}
|
||||
}
|
||||
const translateWheelDrive = (wheelDrive: string) => {
|
||||
switch (wheelDrive) {
|
||||
case "AWD":
|
||||
return "Полный";
|
||||
case "RWD":
|
||||
return "Задний";
|
||||
case "FWD":
|
||||
return "Передний";
|
||||
case "передний":
|
||||
return "Передний";
|
||||
case "задний":
|
||||
return "Задний";
|
||||
case "полный":
|
||||
return "Полный";
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const loadCar = async () => {
|
||||
try {
|
||||
const response = await fetchCarById(Number(id));
|
||||
console.log("Полученные данные автомобиля:", response.car);
|
||||
setCar(response.car);
|
||||
} catch (error) {
|
||||
console.error("Ошибка при загрузке данных:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadCar();
|
||||
}, [id]);
|
||||
|
||||
const handleOpenFeedback = () => {
|
||||
setFeedbackOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseFeedback = () => {
|
||||
setFeedbackOpen(false);
|
||||
};
|
||||
|
||||
// Проверяем, является ли автомобиль электрическим
|
||||
const isElectric = car?.engine_type === "электрический";
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100vh",
|
||||
bgcolor: "#2D2D2D"
|
||||
}}>
|
||||
<CircularProgress sx={{ color: "#C27664" }} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (!car) {
|
||||
return (
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "70vh",
|
||||
bgcolor: "#2D2D2D",
|
||||
color: "white"
|
||||
}}>
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
fontFamily: "Unbounded",
|
||||
fontWeight: "bold",
|
||||
mb: 2
|
||||
}}
|
||||
>
|
||||
Автомобиль не найден
|
||||
</Typography>
|
||||
<Typography>
|
||||
Запрошенный автомобиль не существует или был удален.
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
console.log("Отрисовка автомобиля:", car);
|
||||
console.log("Тип двигателя:", car.engine_type);
|
||||
console.log("Объем двигателя:", car.engine_capacity);
|
||||
console.log("Является электрическим:", isElectric);
|
||||
|
||||
return (
|
||||
<Box sx={{ bgcolor: "#2D2D2D", color: "white", py: isMobile ? "10vw" : "2vw", pb: "12.6vw", overflow: "hidden", userSelect: "none" }}>
|
||||
<Box sx={{ maxWidth: "90vw", margin: "0 auto" }}>
|
||||
{/* Название автомобиля */}
|
||||
{/* <Typography
|
||||
variant="h3"
|
||||
sx={{
|
||||
fontFamily: "Unbounded",
|
||||
fontSize: isMobile ? "8vw" : "4vw",
|
||||
fontWeight: "bold",
|
||||
mb: "2vw",
|
||||
}}
|
||||
>
|
||||
Автомобиль - <span style={{ color: "#C27664" }}>{car.name}</span>
|
||||
</Typography> */}
|
||||
|
||||
<Grid container spacing={isMobile ? 4 : 5}>
|
||||
{/* Изображение автомобиля */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box
|
||||
sx={{
|
||||
width: "54vw",
|
||||
height: isMobile ? "60vw" : "30vw",
|
||||
backgroundImage: `url(${getImageUrl(car.image)})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
borderRadius: "2vw",
|
||||
boxShadow: "0px 0px 20px rgba(0, 0, 0, 0.5)",
|
||||
}}
|
||||
/>
|
||||
<InfoOutlineIcon sx={{
|
||||
position: "absolute",
|
||||
top: "8vw",
|
||||
left: "54vw",
|
||||
width: "3vw",
|
||||
height: "3vw",
|
||||
borderRadius: "1vw",
|
||||
zIndex: 1001,
|
||||
color: "#2D2D2D",
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
scale: "1.1",
|
||||
"&:hover + .info-box": {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
transition: "all 0.3s ease",
|
||||
}} />
|
||||
<Box className="info-box" sx={{
|
||||
padding: "1vw",
|
||||
position: "absolute",
|
||||
top: "10vw",
|
||||
left: "7vw",
|
||||
width: "46vw",
|
||||
backgroundColor: "#2D2D2D",
|
||||
borderRadius: "1.5vw",
|
||||
zIndex: 1000,
|
||||
opacity: 0,
|
||||
transition: "opacity 0.3s ease",
|
||||
boxShadow: "0vw 0vw 1vw 0vw rgba(0, 0, 0, 0.5)",
|
||||
}}>
|
||||
<Typography sx={{ fontSize: isMobile ? "3.5vw" : "1.6vw", fontFamily: "Unbounded" }}>
|
||||
{car.name} {car.year} года выпуска с пробегом {car.mileage.toLocaleString()} км,
|
||||
{car.engine_type && ` ${car.engine_type} двигатель`}
|
||||
{!isElectric && car.engine_capacity ? ` объёмом ${car.engine_capacity} л` : ""}
|
||||
{car.engine_power && `, мощностью ${car.engine_power} л.с.`}
|
||||
{car.drive_type && ` Привод: ${translateWheelDrive(car.drive_type)}.`}
|
||||
{car.country_of_origin && ` Страна производства: ${translateCountry(car.country_of_origin)}.`}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{/* Информация об автомобиле */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box sx={{
|
||||
backgroundColor: "#333333",
|
||||
borderRadius: "2vw",
|
||||
p: isMobile ? "5vw" : "2vw",
|
||||
height: isElectric ? "100%" : "91%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between"
|
||||
}}>
|
||||
<Box>
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: "Unbounded",
|
||||
fontSize: isMobile ? "6vw" : "3vw",
|
||||
fontWeight: "bold",
|
||||
mb: "1vw",
|
||||
color: "#C27664"
|
||||
}}
|
||||
>
|
||||
{car.price.toLocaleString()}₽
|
||||
</Typography>
|
||||
|
||||
<Typography sx={{ fontSize: isMobile ? "3.5vw" : "1.2vw", mb: "0.5vw", fontFamily: "Unbounded" }}>
|
||||
Год выпуска: <strong style={{ color: "#C27664" }}>{car.year}</strong>
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: isMobile ? "3.5vw" : "1.2vw", mb: "0.5vw", fontFamily: "Unbounded" }}>
|
||||
Пробег: <strong style={{ color: "#C27664" }}>{car.mileage.toLocaleString()} км</strong>
|
||||
</Typography>
|
||||
|
||||
{!isElectric && car.engine_capacity !== null && car.engine_capacity !== undefined && (
|
||||
<Typography sx={{ fontSize: isMobile ? "3.5vw" : "1.2vw", mb: "0.5vw", fontFamily: "Unbounded" }}>
|
||||
Объём двигателя: <strong style={{ color: "#C27664" }}>{car.engine_capacity} л</strong>
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{car.engine_power && isElectric && (
|
||||
<Typography sx={{ fontSize: isMobile ? "3.5vw" : "1.2vw", mb: "0.5vw", fontFamily: "Unbounded" }}>
|
||||
Мощность: <strong style={{ color: "#C27664" }}>{Math.round(car.engine_power * 0.735499)} кВт ({car.engine_power} л.с)</strong>
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{car.engine_power && !isElectric && (
|
||||
<Typography sx={{ fontSize: isMobile ? "3.5vw" : "1.2vw", mb: "0.5vw", fontFamily: "Unbounded" }}>
|
||||
Мощность: <strong style={{ color: "#C27664" }}>{car.engine_power} л.с.</strong>
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{car.engine_type && (
|
||||
<Typography sx={{ fontSize: isMobile ? "3.5vw" : "1.2vw", mb: "0.5vw", fontFamily: "Unbounded" }}>
|
||||
Тип двигателя: <strong style={{ color: "#C27664" }}>{car.engine_type}</strong>
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{car.drive_type && (
|
||||
<Typography sx={{ fontSize: isMobile ? "3.5vw" : "1.2vw", mb: "0.5vw", fontFamily: "Unbounded" }}>
|
||||
Привод: <strong style={{ color: "#C27664" }}>{translateWheelDrive(car.drive_type)}</strong>
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{car.country_of_origin && (
|
||||
<Typography sx={{ fontSize: isMobile ? "3.5vw" : "1.2vw", mb: "0.5vw", fontFamily: "Unbounded" }}>
|
||||
Страна производства: <strong style={{ color: "#C27664" }}>{translateCountry(car.country_of_origin)}</strong>
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: isMobile ? "5vw" : "2vw" }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleOpenFeedback}
|
||||
fullWidth
|
||||
sx={{
|
||||
bgcolor: "#C27664",
|
||||
color: "white",
|
||||
fontSize: isMobile ? "4vw" : "1.5vw",
|
||||
padding: isMobile ? "3vw" : "1vw",
|
||||
textTransform: "none",
|
||||
borderRadius: "1vw",
|
||||
fontWeight: "bold",
|
||||
fontFamily: "Unbounded",
|
||||
"&:hover": { bgcolor: "#945B4D" },
|
||||
transition: "all 0.3s ease",
|
||||
mb: "1vw",
|
||||
}}
|
||||
>
|
||||
Заказать автомобиль
|
||||
</Button>
|
||||
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
gap: "1vw",
|
||||
mt: "1vw"
|
||||
}}>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: "#C27664",
|
||||
"&:hover": { color: "#945B4D" },
|
||||
}}
|
||||
>
|
||||
<FaVk size={isMobile ? "7vw" : "2vw"} />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
onClick={() => window.open("https://t.me/autoBROcn", "_blank")}
|
||||
sx={{
|
||||
color: "#C27664",
|
||||
"&:hover": { color: "#945B4D" },
|
||||
}}
|
||||
>
|
||||
<TelegramIcon sx={{ fontSize: isMobile ? "8vw" : "2.5vw" }} />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
sx={{
|
||||
color: "#C27664",
|
||||
"&:hover": { color: "#945B4D" },
|
||||
}}
|
||||
>
|
||||
<WhatsAppIcon sx={{ fontSize: isMobile ? "8vw" : "2.5vw" }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Дополнительная информация */}
|
||||
<Box sx={{ mt: isMobile ? "8vw" : isElectric ? "7.3vw" : "4vw" }}>
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
fontFamily: "Unbounded",
|
||||
fontSize: isMobile ? "6vw" : "2.2vw",
|
||||
fontWeight: "bold",
|
||||
mb: "2vw",
|
||||
mt: "-10vw",
|
||||
backgroundColor: "#333333",
|
||||
borderRadius: "2vw",
|
||||
p: "2vw",
|
||||
maxWidth: "50vw",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{car.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Feedback
|
||||
open={feedbackOpen}
|
||||
onClose={handleCloseFeedback}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CarPage;
|
200
src/pages/ContactsPage.tsx
Normal file
200
src/pages/ContactsPage.tsx
Normal file
@ -0,0 +1,200 @@
|
||||
import React, { useState } from "react";
|
||||
import { Box, Typography, Button, IconButton } from "@mui/material";
|
||||
import { useResponsive } from "../theme/useResponsive";
|
||||
import Feedback from "../components/Feedback";
|
||||
import { scrollToAnchor } from "../utils/scrollUtils";
|
||||
import PhoneIcon from "@mui/icons-material/Phone";
|
||||
import EmailIcon from "@mui/icons-material/Email";
|
||||
import LocationOnIcon from "@mui/icons-material/LocationOn";
|
||||
import TelegramIcon from "@mui/icons-material/Telegram";
|
||||
import { FaVk } from "react-icons/fa";
|
||||
import WhatsAppIcon from "@mui/icons-material/WhatsApp";
|
||||
|
||||
function ContactsPage() {
|
||||
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);
|
||||
};
|
||||
|
||||
const contactIconStyle = {
|
||||
fontSize: isMobile ? "6vw" : "3vw",
|
||||
color: "#C27664",
|
||||
marginRight: "1vw",
|
||||
};
|
||||
|
||||
const contactItemStyle = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginBottom: isMobile ? "4vw" : "2vw",
|
||||
};
|
||||
|
||||
const contactTextStyle = {
|
||||
fontSize: isMobile ? "4vw" : "1.5vw",
|
||||
fontFamily: "Unbounded",
|
||||
};
|
||||
|
||||
const socialIconStyle = {
|
||||
color: "#C27664",
|
||||
borderRadius: "50%",
|
||||
width: isMobile ? "10vw" : "4vw",
|
||||
height: isMobile ? "10vw" : "4vw",
|
||||
border: "2px solid #C27664",
|
||||
margin: isMobile ? "0 3vw" : "0 1vw",
|
||||
"&:hover": { color: "#945B4D", borderColor: "#945B4D" },
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
id="contacts"
|
||||
sx={{
|
||||
bgcolor: "white",
|
||||
color: "black",
|
||||
padding: "5vw",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: "80vw",
|
||||
margin: "0 auto",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
component="h1"
|
||||
sx={{
|
||||
fontFamily: "Unbounded",
|
||||
fontSize: isMobile ? "8vw" : "4rem",
|
||||
fontWeight: "bold",
|
||||
marginBottom: "2rem",
|
||||
textWrap: "nowrap",
|
||||
}}
|
||||
>
|
||||
НАШИ <span style={{ color: "#C27664" }}>КОНТАКТЫ</span>
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: isMobile ? "column" : "row",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: "3vw",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
textAlign: "left",
|
||||
padding: isMobile ? "5vw 0" : "2vw",
|
||||
}}
|
||||
>
|
||||
<Box sx={contactItemStyle}>
|
||||
<PhoneIcon sx={contactIconStyle} />
|
||||
<Typography sx={contactTextStyle}>8 (965) 372-51-90</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={contactItemStyle}>
|
||||
<EmailIcon sx={contactIconStyle} />
|
||||
<Typography sx={contactTextStyle}>info@autobro.ru</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={contactItemStyle}>
|
||||
<LocationOnIcon sx={contactIconStyle} />
|
||||
<Typography sx={contactTextStyle}>
|
||||
г.Москва, станция метро "Домодедово"
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: isMobile ? "8vw" : "4vw",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: isMobile ? "4vw" : "1.5vw",
|
||||
fontFamily: "Unbounded",
|
||||
marginRight: isMobile ? "3vw" : "1vw",
|
||||
}}
|
||||
>
|
||||
Мы в соцсетях:
|
||||
</Typography>
|
||||
<IconButton sx={socialIconStyle}>
|
||||
<FaVk size={isMobile ? "5vw" : "2vw"} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => window.open("https://t.me/autoBROcn", "_blank")}
|
||||
sx={socialIconStyle}
|
||||
>
|
||||
<TelegramIcon sx={{ fontSize: isMobile ? "5vw" : "2vw" }} />
|
||||
</IconButton>
|
||||
<IconButton sx={socialIconStyle}>
|
||||
<WhatsAppIcon sx={{ fontSize: isMobile ? "5vw" : "2vw" }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: isMobile ? "5vw 0" : "2vw",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: "Unbounded",
|
||||
fontSize: isMobile ? "5vw" : "2vw",
|
||||
marginBottom: "2vw",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Остались вопросы?
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleOpenFeedback}
|
||||
sx={{
|
||||
bgcolor: "#C27664",
|
||||
color: "white",
|
||||
fontSize: isMobile ? "3vw" : "1.2vw",
|
||||
padding: isMobile ? "3vw 6vw" : "1.2vw 3vw",
|
||||
textTransform: "none",
|
||||
borderRadius: isMobile ? "3vw" : "1.5vw",
|
||||
fontWeight: "bold",
|
||||
fontFamily: "Unbounded",
|
||||
"&:hover": { bgcolor: "#945B4D" },
|
||||
transition: "all 0.3s ease",
|
||||
}}
|
||||
>
|
||||
Оставить заявку
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Feedback
|
||||
open={feedbackOpen}
|
||||
onClose={handleCloseFeedback}
|
||||
handleScrollToAnchor={handleScrollToAnchor}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default ContactsPage;
|
@ -13,7 +13,8 @@ import CarsPage from "./CarsPage";
|
||||
import DeliveryPage from "./DeliveryPage";
|
||||
import Divider from "../components/Divider";
|
||||
import CalculatorPage from "./CalculatorPage";
|
||||
|
||||
import TeamPage from "./TeamPage";
|
||||
import ContactsPage from "./ContactsPage";
|
||||
function MainPage() {
|
||||
const [feedbackOpen, setFeedbackOpen] = useState(false);
|
||||
const setDrawerOpen = () => {};
|
||||
@ -155,7 +156,6 @@ function MainPage() {
|
||||
</Button>
|
||||
</Box>
|
||||
<Box
|
||||
id="contacts"
|
||||
sx={{
|
||||
// position: "absolute",
|
||||
// top: isMobile ? "18.3vw" : "13.3vw",
|
||||
@ -229,6 +229,10 @@ function MainPage() {
|
||||
<DeliveryPage />
|
||||
<Divider />
|
||||
<CalculatorPage />
|
||||
<Divider marginTopDivider={isMobile ? "10vw" : "1vw"} marginBottomDivider={isMobile ? "10vw" : "1vw"}/>
|
||||
<TeamPage />
|
||||
<Divider />
|
||||
<ContactsPage />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
276
src/pages/TeamPage.tsx
Normal file
276
src/pages/TeamPage.tsx
Normal file
@ -0,0 +1,276 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Box, Typography, CircularProgress } from "@mui/material";
|
||||
import { useResponsive } from "../theme/useResponsive";
|
||||
import { fetchTeam, getImageUrl } from "../utils/api";
|
||||
import type { TeamMember } from "../utils/api";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
|
||||
function TeamPage() {
|
||||
const { isMobile } = useResponsive();
|
||||
const [teamMembers, setTeamMembers] = useState<TeamMember[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
|
||||
// Загрузка данных о команде с бэкенда
|
||||
useEffect(() => {
|
||||
const loadTeam = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetchTeam();
|
||||
setTeamMembers(response.staff);
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
console.error("Ошибка при загрузке данных о команде:", err);
|
||||
setError("Не удалось загрузить информацию о команде");
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadTeam();
|
||||
}, []);
|
||||
|
||||
// Получаем индексы для отображения (центральный и боковые)
|
||||
const getVisibleIndices = () => {
|
||||
if (teamMembers.length === 0) return { prev: -1, current: -1, next: -1 };
|
||||
|
||||
const prev = activeIndex === 0 ? teamMembers.length - 1 : activeIndex - 1;
|
||||
const current = activeIndex;
|
||||
const next = activeIndex === teamMembers.length - 1 ? 0 : activeIndex + 1;
|
||||
|
||||
return { prev, current, next };
|
||||
};
|
||||
|
||||
// Обработчик клика по сотруднику
|
||||
const handleMemberClick = (index: number) => {
|
||||
setActiveIndex(index);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
id="team"
|
||||
sx={{
|
||||
bgcolor: "white",
|
||||
color: "black",
|
||||
padding: isMobile ? "5vw 0vw 5vw 0vw" : "5vw",
|
||||
scrollMarginTop: isMobile ? "15vw" : "10vw",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: isMobile ? "100vw" : "90vw",
|
||||
margin: "0 auto",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
component="h1"
|
||||
sx={{
|
||||
fontFamily: "Unbounded",
|
||||
fontSize: isMobile ? "8vw" : "5vw",
|
||||
fontWeight: "bold",
|
||||
textAlign: isMobile ? "center" : "left",
|
||||
}}
|
||||
>
|
||||
Наша <span style={{ color: "#C27664" }}>команда</span>
|
||||
</Typography>
|
||||
|
||||
{loading ? (
|
||||
<Box sx={{ display: "flex", justifyContent: "center", my: 5 }}>
|
||||
<CircularProgress sx={{ color: "#C27664" }} />
|
||||
</Box>
|
||||
) : error ? (
|
||||
<Typography color="error" sx={{ textAlign: "center", my: 5 }}>
|
||||
{error}
|
||||
</Typography>
|
||||
) : teamMembers.length > 0 ? (
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
height: isMobile ? "110vw" : "40vw",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
overflow: isMobile ? "hidden" : "visible",
|
||||
top: isMobile ? "auto" : "-5vw",
|
||||
}}
|
||||
>
|
||||
<AnimatePresence>
|
||||
{/* Карусель с сотрудниками */}
|
||||
{teamMembers.map((member, index) => {
|
||||
const { prev, current, next } = getVisibleIndices();
|
||||
let position: "prev" | "current" | "next" | "hidden" = "hidden";
|
||||
|
||||
if (index === prev) position = "prev";
|
||||
else if (index === current) position = "current";
|
||||
else if (index === next) position = "next";
|
||||
|
||||
// Если элемент не видим, не рендерим его
|
||||
if (position === "hidden") return null;
|
||||
|
||||
// Настройки для разных позиций
|
||||
const positionStyles = {
|
||||
prev: {
|
||||
left: isMobile ? "20%" : "30%",
|
||||
top: isMobile ? "40%" : "50%",
|
||||
zIndex: 1,
|
||||
scale: 0.8,
|
||||
opacity: 0.7,
|
||||
},
|
||||
current: {
|
||||
left: "50%",
|
||||
top: "50%",
|
||||
zIndex: 2,
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
},
|
||||
next: {
|
||||
left: isMobile ? "80%" : "70%",
|
||||
top: isMobile ? "40%" : "50%",
|
||||
zIndex: 1,
|
||||
scale: 0.8,
|
||||
opacity: 0.7,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={member.id}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{
|
||||
x: position === "current" ? "-50%" : position === "prev" ? "-100%" : "0%",
|
||||
y: position === "current" ? "-50%" : "-50%",
|
||||
scale: positionStyles[position].scale,
|
||||
opacity: positionStyles[position].opacity,
|
||||
zIndex: positionStyles[position].zIndex,
|
||||
textWrap: "nowrap",
|
||||
}}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
onClick={() => position !== "current" && handleMemberClick(index)}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: positionStyles[position].left,
|
||||
top: positionStyles[position].top,
|
||||
cursor: position !== "current" ? "pointer" : "default",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: isMobile
|
||||
? position === "current" ? "60vw" : "40vw"
|
||||
: position === "current" ? "25vw" : "20vw",
|
||||
height: isMobile
|
||||
? position === "current" ? "60vw" : "40vw"
|
||||
: position === "current" ? "25vw" : "20vw",
|
||||
// borderRadius: "50%",
|
||||
overflow: "hidden",
|
||||
mb: "1vw",
|
||||
// boxShadow: position === "current" ? "0px 4px 20px rgba(0, 0, 0, 0.2)" : "none",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ position: 'relative', width: '100%', height: '100%' }}>
|
||||
<img
|
||||
src={getImageUrl(member.photo)}
|
||||
alt={`${member.name} ${member.surname}`}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '30%',
|
||||
background: 'linear-gradient(to top, rgba(255,255,255,1) 0%, rgba(255,255,255,0) 100%)',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: "Unbounded",
|
||||
fontSize: isMobile
|
||||
? position === "current" ? "5vw" : "3.5vw"
|
||||
: position === "current" ? "1.5rem" : "1.2rem",
|
||||
color: "#C27664",
|
||||
fontWeight: "bold",
|
||||
mb: 1,
|
||||
visibility: "visible",
|
||||
}}
|
||||
>
|
||||
{member.name} {member.surname}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: "Unbounded",
|
||||
fontSize: isMobile
|
||||
? position === "current" ? "3.5vw" : "2.5vw"
|
||||
: position === "current" ? "1.2rem" : "1rem",
|
||||
color: "black",
|
||||
visibility: "visible",
|
||||
}}
|
||||
>
|
||||
{member.role}
|
||||
</Typography>
|
||||
</Box>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Навигационные точки */}
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
bottom: "-5vw",
|
||||
top: "50%",
|
||||
display: "flex",
|
||||
gap: "1vw",
|
||||
}}
|
||||
>
|
||||
{teamMembers.map((_, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
onClick={() => handleMemberClick(index)}
|
||||
sx={{
|
||||
width: isMobile ? "3vw" : "1vw",
|
||||
height: isMobile ? "3vw" : "1vw",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: index === activeIndex ? "#C27664" : "#CDCDCD",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: "Unbounded",
|
||||
fontSize: isMobile ? "4vw" : "1.5rem",
|
||||
textAlign: "center",
|
||||
my: 5,
|
||||
}}
|
||||
>
|
||||
Информация о команде скоро появится
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default TeamPage;
|
125
src/utils/api.ts
125
src/utils/api.ts
@ -1,10 +1,9 @@
|
||||
// utils/api.ts
|
||||
|
||||
// Типы данных
|
||||
|
||||
// Типы данных для автомобилей
|
||||
export interface Car {
|
||||
id: number;
|
||||
make: string;
|
||||
model: string;
|
||||
name: string;
|
||||
year: number;
|
||||
mileage: number;
|
||||
price: number;
|
||||
@ -20,6 +19,24 @@ export interface CarResponse {
|
||||
car: Car;
|
||||
}
|
||||
|
||||
// Типы данных для персонала
|
||||
export interface TeamMember {
|
||||
id: number;
|
||||
name: string;
|
||||
surname: string;
|
||||
role: string;
|
||||
photo: string;
|
||||
}
|
||||
|
||||
export interface TeamResponse {
|
||||
staff: TeamMember[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface TeamMemberResponse {
|
||||
personal: TeamMember;
|
||||
}
|
||||
|
||||
// Базовый URL API
|
||||
const API_BASE_URL = 'http://localhost:8000';
|
||||
|
||||
@ -129,3 +146,103 @@ export const getImageUrl = (imagePath: string | null): string => {
|
||||
if (!imagePath) return '/placeholder.jpg';
|
||||
return `${API_BASE_URL}${imagePath}`;
|
||||
};
|
||||
|
||||
// ФУНКЦИИ ДЛЯ РАБОТЫ С ПЕРСОНАЛОМ
|
||||
|
||||
// Получение списка сотрудников
|
||||
export const fetchTeam = async (skip = 0, limit = 100): Promise<TeamResponse> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/personal?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 fetchTeamMemberById = async (memberId: number): Promise<TeamMemberResponse> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/personal/${memberId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Ошибка HTTP: ${response.status}`);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
return handleApiError(error);
|
||||
}
|
||||
};
|
||||
|
||||
// Добавление нового сотрудника
|
||||
export const createTeamMember = async (
|
||||
memberData: Omit<TeamMember, 'id'>,
|
||||
photo?: File
|
||||
): Promise<TeamMemberResponse> => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('personal_data', JSON.stringify(memberData));
|
||||
|
||||
if (photo) {
|
||||
formData.append('photo', photo);
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/personal`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Ошибка HTTP: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
return handleApiError(error);
|
||||
}
|
||||
};
|
||||
|
||||
// Обновление информации о сотруднике
|
||||
export const updateTeamMember = async (
|
||||
memberId: number,
|
||||
memberData: Partial<Omit<TeamMember, 'id'>>,
|
||||
photo?: File
|
||||
): Promise<TeamMemberResponse> => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('personal_data', JSON.stringify(memberData));
|
||||
|
||||
if (photo) {
|
||||
formData.append('photo', photo);
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/personal/${memberId}`, {
|
||||
method: 'PUT',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Ошибка HTTP: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
return handleApiError(error);
|
||||
}
|
||||
};
|
||||
|
||||
// Удаление сотрудника
|
||||
export const deleteTeamMember = async (memberId: number): Promise<void> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/personal/${memberId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Ошибка HTTP: ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return handleApiError(error);
|
||||
}
|
||||
};
|
||||
|
@ -9,6 +9,9 @@ export const scrollToAnchor = (
|
||||
setDrawerOpen?: (isOpen: boolean) => void,
|
||||
isMobile?: boolean
|
||||
): void => {
|
||||
if (location.pathname.startsWith("/car")) {
|
||||
navigate("/");
|
||||
}
|
||||
const element = document.querySelector(anchor);
|
||||
if (element) {
|
||||
// Получаем высоту хедера (в соответствии с размерами в Header.tsx)
|
||||
|
Reference in New Issue
Block a user