diff --git a/src/App.tsx b/src/App.tsx index 145ca18..b16dc74 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { } /> + } + /> ) diff --git a/src/components/Divider.tsx b/src/components/Divider.tsx index 512e68d..bdb72ae 100644 --- a/src/components/Divider.tsx +++ b/src/components/Divider.tsx @@ -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 ( - + diff --git a/src/components/Feedback.tsx b/src/components/Feedback.tsx index c0c665f..a915291 100644 --- a/src/components/Feedback.tsx +++ b/src/components/Feedback.tsx @@ -62,7 +62,7 @@ const Feedback: React.FC = ({ }) => { 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 = ({ value={country} onChange={(e) => setCountry(e.target.value)} > - - } - label={ - - Европа - - } - sx={{ - marginRight: isMobile ? "2.5vw" : "1.5vw", - ml: "0vw", - }} - /> { 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 = () => { {/* Пустой блок для компенсации фиксированного хедера */} - + {/* Мобильное меню */} diff --git a/src/pages/CalculatorPage.tsx b/src/pages/CalculatorPage.tsx index 3c7aa9a..e2f2b43 100644 --- a/src/pages/CalculatorPage.tsx +++ b/src/pages/CalculatorPage.tsx @@ -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", }} > setEnhancedPermeability(!enhancedPermeability) diff --git a/src/pages/CarPage.tsx b/src/pages/CarPage.tsx new file mode 100644 index 0000000..9d52535 --- /dev/null +++ b/src/pages/CarPage.tsx @@ -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(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 ( + + + + ); + } + + if (!car) { + return ( + + + Автомобиль не найден + + + Запрошенный автомобиль не существует или был удален. + + + ); + } + + console.log("Отрисовка автомобиля:", car); + console.log("Тип двигателя:", car.engine_type); + console.log("Объем двигателя:", car.engine_capacity); + console.log("Является электрическим:", isElectric); + + return ( + + + {/* Название автомобиля */} + {/* + Автомобиль - {car.name} + */} + + + {/* Изображение автомобиля */} + + + + + + {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)}.`} + + + + + {/* Информация об автомобиле */} + + + + + {car.price.toLocaleString()}₽ + + + + Год выпуска: {car.year} + + + Пробег: {car.mileage.toLocaleString()} км + + + {!isElectric && car.engine_capacity !== null && car.engine_capacity !== undefined && ( + + Объём двигателя: {car.engine_capacity} л + + )} + + {car.engine_power && isElectric && ( + + Мощность: {Math.round(car.engine_power * 0.735499)} кВт ({car.engine_power} л.с) + + )} + + {car.engine_power && !isElectric && ( + + Мощность: {car.engine_power} л.с. + + )} + + {car.engine_type && ( + + Тип двигателя: {car.engine_type} + + )} + + {car.drive_type && ( + + Привод: {translateWheelDrive(car.drive_type)} + + )} + + {car.country_of_origin && ( + + Страна производства: {translateCountry(car.country_of_origin)} + + )} + + + + + + + + + + + window.open("https://t.me/autoBROcn", "_blank")} + sx={{ + color: "#C27664", + "&:hover": { color: "#945B4D" }, + }} + > + + + + + + + + + + + + + {/* Дополнительная информация */} + + + {car.name} + + + + + + + ); +} + +export default CarPage; \ No newline at end of file diff --git a/src/pages/ContactsPage.tsx b/src/pages/ContactsPage.tsx new file mode 100644 index 0000000..467fbd0 --- /dev/null +++ b/src/pages/ContactsPage.tsx @@ -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 ( + + + + НАШИ КОНТАКТЫ + + + + + + + 8 (965) 372-51-90 + + + + + info@autobro.ru + + + + + + г.Москва, станция метро "Домодедово" + + + + + + Мы в соцсетях: + + + + + window.open("https://t.me/autoBROcn", "_blank")} + sx={socialIconStyle} + > + + + + + + + + + + + Остались вопросы? + + + + + + + + + ); +} + +export default ContactsPage; \ No newline at end of file diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx index f1b4855..662ba12 100644 --- a/src/pages/MainPage.tsx +++ b/src/pages/MainPage.tsx @@ -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() { + + + + ); } diff --git a/src/pages/TeamPage.tsx b/src/pages/TeamPage.tsx new file mode 100644 index 0000000..fae38fc --- /dev/null +++ b/src/pages/TeamPage.tsx @@ -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([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 ( + + + + Наша команда + + + {loading ? ( + + + + ) : error ? ( + + {error} + + ) : teamMembers.length > 0 ? ( + + + {/* Карусель с сотрудниками */} + {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 ( + position !== "current" && handleMemberClick(index)} + style={{ + position: "absolute", + left: positionStyles[position].left, + top: positionStyles[position].top, + cursor: position !== "current" ? "pointer" : "default", + }} + > + + + + {`${member.name} + + + + + {member.name} {member.surname} + + + {member.role} + + + + ); + })} + + + {/* Навигационные точки */} + + {teamMembers.map((_, index) => ( + handleMemberClick(index)} + sx={{ + width: isMobile ? "3vw" : "1vw", + height: isMobile ? "3vw" : "1vw", + borderRadius: "50%", + backgroundColor: index === activeIndex ? "#C27664" : "#CDCDCD", + cursor: "pointer", + }} + /> + ))} + + + ) : ( + + Информация о команде скоро появится + + )} + + + ); +} + +export default TeamPage; \ No newline at end of file diff --git a/src/utils/api.ts b/src/utils/api.ts index dc84700..6637113 100644 --- a/src/utils/api.ts +++ b/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 => { + 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 => { + 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, + photo?: File +): Promise => { + 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>, + photo?: File +): Promise => { + 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 => { + 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); + } +}; diff --git a/src/utils/scrollUtils.ts b/src/utils/scrollUtils.ts index 9431e61..9be1128 100644 --- a/src/utils/scrollUtils.ts +++ b/src/utils/scrollUtils.ts @@ -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)