add persons, / footer(contacts) / main car page

This commit is contained in:
aurinex
2025-07-12 01:46:43 +05:00
parent 643960cadb
commit 2bc57b7d0b
11 changed files with 1011 additions and 50 deletions

View File

@ -1,5 +1,6 @@
import { Routes, Route } from 'react-router-dom' import { Routes, Route } from 'react-router-dom'
import MainPage from './pages/MainPage.tsx' import MainPage from './pages/MainPage.tsx'
import CarPage from './pages/CarPage.tsx'
import Header from './components/Header.tsx' import Header from './components/Header.tsx'
function App() { function App() {
@ -15,6 +16,10 @@ function App() {
</> </>
} }
/> />
<Route
path="/car/:id"
element={<CarPage />}
/>
</Routes> </Routes>
</> </>
) )

View File

@ -2,10 +2,15 @@ import { Box } from "@mui/material";
import React from "react"; import React from "react";
import { useResponsive } from "../theme/useResponsive"; import { useResponsive } from "../theme/useResponsive";
function Divider() { interface DividerProps {
marginTopDivider?: string | "1vw";
marginBottomDivider?: string | "1vw";
}
function Divider({ marginTopDivider, marginBottomDivider }: DividerProps) {
const { isMobile } = useResponsive(); const { isMobile } = useResponsive();
return ( return (
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "center" }}> <Box sx={{ display: "flex", alignItems: "center", justifyContent: "center", bgcolor: "#fff" }}>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
@ -14,9 +19,9 @@ function Divider() {
height: isMobile ? "0.3vw" : "0.15vw", height: isMobile ? "0.3vw" : "0.15vw",
backgroundColor: "rgba(220, 220, 220, 1)", backgroundColor: "rgba(220, 220, 220, 1)",
borderRadius: "1vw", borderRadius: "1vw",
position: "absolute", // position: "absolute",
mt: "1vw", mt: marginTopDivider,
mb: "1vw", mb: marginBottomDivider,
}} }}
></Box> ></Box>
</Box> </Box>

View File

@ -62,7 +62,7 @@ const Feedback: React.FC<FeedbackProps> = ({
}) => { }) => {
const [name, setName] = useState(""); const [name, setName] = useState("");
const [phone, setPhone] = useState("+7"); const [phone, setPhone] = useState("+7");
const [country, setCountry] = useState("Европа"); const [country, setCountry] = useState("США");
const [budget, setBudget] = useState("до 3 млн"); const [budget, setBudget] = useState("до 3 млн");
const [description, setDescription] = useState(""); const [description, setDescription] = useState("");
const [agreeToPolicy, setAgreeToPolicy] = useState(false); const [agreeToPolicy, setAgreeToPolicy] = useState(false);
@ -271,35 +271,6 @@ const Feedback: React.FC<FeedbackProps> = ({
value={country} value={country}
onChange={(e) => setCountry(e.target.value)} 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 <FormControlLabel
value="США" value="США"
control={ control={

View File

@ -14,22 +14,25 @@ import logo from "../assets/icon/autobro.png";
import { useResponsive } from "../theme/useResponsive"; import { useResponsive } from "../theme/useResponsive";
import Feedback from "./Feedback"; import Feedback from "./Feedback";
import { scrollToAnchor } from "../utils/scrollUtils"; import { scrollToAnchor } from "../utils/scrollUtils";
import { useNavigate } from "react-router-dom";
const Header = () => { const Header = () => {
const { isMobile } = useResponsive(); const { isMobile } = useResponsive();
const [drawerOpen, setDrawerOpen] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false);
const [feedbackOpen, setFeedbackOpen] = useState(false); const [feedbackOpen, setFeedbackOpen] = useState(false);
const [scrolled, setScrolled] = useState(false); const [scrolled, setScrolled] = useState(false);
const navigate = useNavigate();
const menuItems = [ const menuItems = [
// { title: "", anchor: "#main" },
{ title: "О нас", anchor: "#about-us" }, { title: "О нас", anchor: "#about-us" },
{ title: "Этапы работы", anchor: "#stages" },
{ title: "В наличии", anchor: "#available" },
{ title: "Калькулятор", anchor: "#calculator" }, { title: "Калькулятор", anchor: "#calculator" },
{ title: "Команда", anchor: "#team" },
{ title: "Отзывы", anchor: "#reviews" }, { title: "Отзывы", anchor: "#reviews" },
{ title: "Контакты", anchor: "#contacts" }, { title: "Контакты", anchor: "#contacts" },
{ title: "В наличии", anchor: "#available" }, // { title: "Доставленные авто", anchor: "#delivered" },
{ title: "Команда", anchor: "#team" },
{ title: "Доставленные авто", anchor: "#delivered" },
{ title: "Этапы работы", anchor: "#stages" },
{ title: " ", anchor: "#main" },
]; ];
// Отслеживание скролла // Отслеживание скролла
@ -62,7 +65,11 @@ const Header = () => {
// Используем глобальную функцию из utils // Используем глобальную функцию из utils
const handleScrollToAnchor = (anchor: string) => { const handleScrollToAnchor = (anchor: string) => {
scrollToAnchor(anchor, setDrawerOpen, isMobile); if (location.pathname.startsWith("/car")) {
navigate("/");
} else {
scrollToAnchor(anchor, setDrawerOpen, isMobile);
}
}; };
return ( return (
@ -153,7 +160,7 @@ const Header = () => {
</Box> </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)}> <Drawer anchor="right" open={drawerOpen} onClose={toggleDrawer(false)}>

View File

@ -13,6 +13,7 @@ import {
Collapse, Collapse,
Paper, Paper,
Slide, Slide,
Checkbox,
} from "@mui/material"; } from "@mui/material";
import { useResponsive } from "../theme/useResponsive"; import { useResponsive } from "../theme/useResponsive";
import russia from "../assets/emoji/russia.png"; import russia from "../assets/emoji/russia.png";
@ -330,7 +331,6 @@ function CalculatorPage() {
scrollMarginTop: isMobile ? "15vw" : "10vw", scrollMarginTop: isMobile ? "15vw" : "10vw",
maxWidth: "100vw", maxWidth: "100vw",
overflowX: "hidden", overflowX: "hidden",
pb: "25vw",
}} }}
> >
<Box <Box
@ -1125,7 +1125,7 @@ function CalculatorPage() {
> >
<FormControlLabel <FormControlLabel
control={ control={
<Radio <Checkbox
checked={enhancedPermeability} checked={enhancedPermeability}
onChange={() => onChange={() =>
setEnhancedPermeability(!enhancedPermeability) setEnhancedPermeability(!enhancedPermeability)

373
src/pages/CarPage.tsx Normal file
View 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
View 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;

View File

@ -13,7 +13,8 @@ import CarsPage from "./CarsPage";
import DeliveryPage from "./DeliveryPage"; import DeliveryPage from "./DeliveryPage";
import Divider from "../components/Divider"; import Divider from "../components/Divider";
import CalculatorPage from "./CalculatorPage"; import CalculatorPage from "./CalculatorPage";
import TeamPage from "./TeamPage";
import ContactsPage from "./ContactsPage";
function MainPage() { function MainPage() {
const [feedbackOpen, setFeedbackOpen] = useState(false); const [feedbackOpen, setFeedbackOpen] = useState(false);
const setDrawerOpen = () => {}; const setDrawerOpen = () => {};
@ -155,7 +156,6 @@ function MainPage() {
</Button> </Button>
</Box> </Box>
<Box <Box
id="contacts"
sx={{ sx={{
// position: "absolute", // position: "absolute",
// top: isMobile ? "18.3vw" : "13.3vw", // top: isMobile ? "18.3vw" : "13.3vw",
@ -229,6 +229,10 @@ function MainPage() {
<DeliveryPage /> <DeliveryPage />
<Divider /> <Divider />
<CalculatorPage /> <CalculatorPage />
<Divider marginTopDivider={isMobile ? "10vw" : "1vw"} marginBottomDivider={isMobile ? "10vw" : "1vw"}/>
<TeamPage />
<Divider />
<ContactsPage />
</Box> </Box>
); );
} }

276
src/pages/TeamPage.tsx Normal file
View 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;

View File

@ -1,10 +1,9 @@
// utils/api.ts
// Типы данных
// Типы данных для автомобилей
export interface Car { export interface Car {
id: number; id: number;
make: string; name: string;
model: string;
year: number; year: number;
mileage: number; mileage: number;
price: number; price: number;
@ -20,6 +19,24 @@ export interface CarResponse {
car: Car; 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 // Базовый URL API
const API_BASE_URL = 'http://localhost:8000'; const API_BASE_URL = 'http://localhost:8000';
@ -129,3 +146,103 @@ export const getImageUrl = (imagePath: string | null): string => {
if (!imagePath) return '/placeholder.jpg'; if (!imagePath) return '/placeholder.jpg';
return `${API_BASE_URL}${imagePath}`; 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);
}
};

View File

@ -9,6 +9,9 @@ export const scrollToAnchor = (
setDrawerOpen?: (isOpen: boolean) => void, setDrawerOpen?: (isOpen: boolean) => void,
isMobile?: boolean isMobile?: boolean
): void => { ): void => {
if (location.pathname.startsWith("/car")) {
navigate("/");
}
const element = document.querySelector(anchor); const element = document.querySelector(anchor);
if (element) { if (element) {
// Получаем высоту хедера (в соответствии с размерами в Header.tsx) // Получаем высоту хедера (в соответствии с размерами в Header.tsx)