Commenting code and bringing it to the same style

This commit is contained in:
RailTH
2024-05-19 14:37:37 +11:00
parent 9519f45c5f
commit 730639e280
18 changed files with 436 additions and 500 deletions

View File

@@ -1,7 +1,7 @@
{ {
"files": { "files": {
"main.css": "/static/css/main.1aa814f6.css", "main.css": "/static/css/main.1aa814f6.css",
"main.js": "/static/js/main.47a170f7.js", "main.js": "/static/js/main.0ec10cd6.js",
"static/media/scam-image.png": "/static/media/scam-image.c6c14289dc251ba2d2b1.png", "static/media/scam-image.png": "/static/media/scam-image.c6c14289dc251ba2d2b1.png",
"static/media/info-page__railth-avatar.png": "/static/media/info-page__railth-avatar.cbf11c43b5ef243b38c0.png", "static/media/info-page__railth-avatar.png": "/static/media/info-page__railth-avatar.cbf11c43b5ef243b38c0.png",
"static/media/add.webp": "/static/media/add.cd69f1e2a8c91109db0f.webp", "static/media/add.webp": "/static/media/add.cd69f1e2a8c91109db0f.webp",
@@ -14,10 +14,10 @@
"static/media/rating__filled-star-icon.svg": "/static/media/rating__filled-star-icon.dc7d908d4d943b7f3b56.svg", "static/media/rating__filled-star-icon.svg": "/static/media/rating__filled-star-icon.dc7d908d4d943b7f3b56.svg",
"index.html": "/index.html", "index.html": "/index.html",
"main.1aa814f6.css.map": "/static/css/main.1aa814f6.css.map", "main.1aa814f6.css.map": "/static/css/main.1aa814f6.css.map",
"main.47a170f7.js.map": "/static/js/main.47a170f7.js.map" "main.0ec10cd6.js.map": "/static/js/main.0ec10cd6.js.map"
}, },
"entrypoints": [ "entrypoints": [
"static/css/main.1aa814f6.css", "static/css/main.1aa814f6.css",
"static/js/main.47a170f7.js" "static/js/main.0ec10cd6.js"
] ]
} }

View File

@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>SusMarket</title><link rel="manifest" href="/manifest.json"/><script defer="defer" src="/static/js/main.47a170f7.js"></script><link href="/static/css/main.1aa814f6.css" rel="stylesheet"></head><body><div id="root"></div></body></html> <!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>SusMarket</title><link rel="manifest" href="/manifest.json"/><script defer="defer" src="/static/js/main.0ec10cd6.js"></script><link href="/static/css/main.1aa814f6.css" rel="stylesheet"></head><body><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -12,92 +12,67 @@ import PopupMap from "./components/PopupMap";
import { Product, Category } from "./utils/types"; import { Product, Category } from "./utils/types";
interface AppPopupMapState { interface AppPopupMapState {
isPopupMapVisible: boolean; isPopupMapVisible: boolean;
} }
export default function App() { export default function App() {
// Состояние для отображения/скрытия карты const [state, setState] = useState<AppPopupMapState>({ isPopupMapVisible: false });
const [state, setState] = useState<AppPopupMapState>({ const [products, setProducts] = useState<Product[]>([]);
isPopupMapVisible: false, const [selectedCategory, setSelectedCategory] = useState<Category | 'all'>('all');
}); const [searchQuery, setSearchQuery] = useState('');
// Массив товаров useEffect(() => {
const [products, setProducts] = useState<Product[]>([]); axios.get('http://127.0.0.1:8000/api/get/products')
// Выбранная категория или все категории .then(response => {
const [selectedCategory, setSelectedCategory] = useState<Category | 'all'>('all'); setProducts(response.data.products);
// Поисковый запрос })
const [searchQuery, setSearchQuery] = useState(''); .catch(error => {
console.error('Error fetching the products:', error);
});
}, []);
// Получение товаров при загрузке компонента const togglePopupMap = () => {
useEffect(() => { setState(prevState => {
axios.get('http://127.0.0.1:8000/api/get/products') if (!prevState.isPopupMapVisible) {
.then(response => { document.body.classList.add('no-scroll');
setProducts(response.data.products); } else {
}) document.body.classList.remove('no-scroll');
.catch(error => { }
console.error('There was an error fetching the products', error); return { ...prevState, isPopupMapVisible: !prevState.isPopupMapVisible };
}); });
}, []); };
// Функция для переключения отображения/скрытия карты const handleSearchChange = (query: string) => {
const togglePopupMap = () => { setSearchQuery(query);
setState((prevState) => { };
if (!prevState.isPopupMapVisible) {
document.body.classList.add('no-scroll');
} else {
document.body.classList.remove('no-scroll');
}
return {
...prevState,
isPopupMapVisible: !prevState.isPopupMapVisible,
};
});
};
// Обработчик изменения поискового запроса const filteredProducts = products.filter(product =>
const handleSearchChange = (query: string) => { (selectedCategory === 'all' || product.category_id === selectedCategory.id) &&
setSearchQuery(query); product.title.toLowerCase().includes(searchQuery.toLowerCase())
}; );
// Фильтрация продуктов по выбранной категории и поисковому запросу const handleSelectCategory = (category: Category | 'all') => {
const filteredProducts = products.filter(product => setSelectedCategory(category);
(selectedCategory === 'all' || product.category_id === selectedCategory.id) && };
product.title.toLowerCase().includes(searchQuery.toLowerCase())
);
// Обработчик выбора категории
const handleSelectCategory = (category: Category | 'all') => {
setSelectedCategory(category);
};
return (
<>
{/* Шапка сайта */}
<Header
togglePopupMap={togglePopupMap}
onSelectCategory={handleSelectCategory}
onSearchChange={handleSearchChange}
/>
{/* Карта */}
{state.isPopupMapVisible && <PopupMap togglePopupMap={togglePopupMap}/>}
{/* Основной контент */}
<main className="main">
<Routes>
{/* Главная страница */}
<Route path="/" element={<HomePage products={filteredProducts}/>}/>
{/* Страница профиля */}
<Route path="profile/*" element={<ProfilePage />}/>
{/* Страница продукта */}
<Route path="product/:id" element={<ProductPage/>}/>
{/* Страница оплаты */}
<Route path="payment" element={<PaymentPage />}/>
{/* Страница с описанием */}
<Route path="scam" element={<ScamPage />}/>
{/* Страница информации */}
<Route path="info" element={<InfoPage />}/>
</Routes>
</main>
</>
);
}
return (
<>
<Header
togglePopupMap={togglePopupMap}
onSelectCategory={handleSelectCategory}
onSearchChange={handleSearchChange}
/>
{state.isPopupMapVisible && <PopupMap togglePopupMap={togglePopupMap} />}
<main className="main">
<Routes>
<Route path="/" element={<HomePage products={filteredProducts} />} />
<Route path="profile/*" element={<ProfilePage />} />
<Route path="product/:id" element={<ProductPage />} />
<Route path="payment" element={<PaymentPage />} />
<Route path="scam" element={<ScamPage />} />
<Route path="info" element={<InfoPage />} />
</Routes>
</main>
</>
);
}

View File

@@ -1,44 +1,46 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from 'react';
import axios from "axios"; import axios from 'axios';
import { Category } from "../utils/types"; import { Category } from '../utils/types';
interface CatalogMenuProps { interface CatalogMenuProps { // Пропсы, которые компонент принимает
toggleCatalogMenu: () => void; toggleCatalogMenu: () => void; // Функция для закрытия меню
onSelectCategory: (category: Category | 'all') => void; onSelectCategory: (category: Category | 'all') => void; // Функция для выбора категории
} }
export default function CatalogMenu({ toggleCatalogMenu, onSelectCategory }: CatalogMenuProps): JSX.Element { export default function CatalogMenu({ toggleCatalogMenu, onSelectCategory }: CatalogMenuProps) {
const [categories, setCategories] = useState<Category[]>([]); const [categories, setCategories] = useState<Category[]>([]); // Состояние для хранения категорий
useEffect(() => { useEffect(() => { // При монтировании компонента запрашиваем категории с сервера
axios.get('http://127.0.0.1:8000/api/get/category') axios.get('http://127.0.0.1:8000/api/get/category')
.then(response => { .then(response => {
setCategories(response.data.categories); setCategories(response.data.categories);
}) })
.catch(error => { .catch(error => {
console.error(`There was an error retrieving the data: ${error}`); console.error(`Ошибка при получении данных: ${error}`); // Обрабатываем ошибку
}); });
}, []); }, []);
return ( return (
<> <>
<div className="background-blackout" onClick={toggleCatalogMenu}></div> {/* Задний фон для закрытия меню */}
<ul className="catalog-menu"> <div className="background-blackout" onClick={toggleCatalogMenu}></div>
{categories.map((category) => ( {/* Список категорий */}
<li <ul className="catalog-menu">
key={category.id} {categories.map((category) => (
className="catalog-menu__point-li" <li
onClick={() => onSelectCategory(category)} key={category.id}
> className="catalog-menu__point-li"
<img onClick={() => onSelectCategory(category)} // Выбор категории
className="catalog-menu__category-icon" >
src={category.image} <img
alt={category.title} className="catalog-menu__category-icon"
/> src={category.image}
{category.title} alt={category.title}
</li> />
))} {category.title}
</ul> </li>
</> ))}
); </ul>
} </>
);
}

View File

@@ -1,48 +1,40 @@
import React, { useState } from "react"; import React, { useState } from 'react';
import { motion } from "framer-motion"; import { motion } from 'framer-motion';
import { Link } from "react-router-dom"; import { Link, useNavigate } from 'react-router-dom';
import Logotype from "../assets/img/amongasik.png"; import Logotype from '../assets/img/amongasik.png';
import CatalogMenu from "./CatalogMenu"; import CatalogMenu from './CatalogMenu';
import LoginMenu from "./LoginMenu"; import LoginMenu from './LoginMenu';
import { Category } from "../utils/types"; import { Category } from '../utils/types';
import Cookies from "js-cookie"; import Cookies from 'js-cookie';
import { useNavigate } from "react-router-dom";
interface HeaderProps { interface HeaderProps { // Интерфейс для пропсов компонента Header
togglePopupMap: () => void; togglePopupMap: () => void; // Функция для переключения видимости карты
onSelectCategory: (category: Category | 'all') => void; onSelectCategory: (category: Category | 'all') => void; // Функция для выбора категории
onSearchChange: (query: string) => void; onSearchChange: (query: string) => void; // Функция для изменения строки поиска
} }
const MotionLink = motion(Link); const MotionLink = motion(Link); // Вынесение компонента в отдельную переменную для удобства использования
export default function Header({ togglePopupMap, onSelectCategory, onSearchChange }: HeaderProps): JSX.Element { export default function Header({ togglePopupMap, onSelectCategory, onSearchChange }: HeaderProps) {
const [isCatalogMenuVisible, setIsCatalogMenuVisible] = useState(false); const [isCatalogMenuVisible, setIsCatalogMenuVisible] = useState(false); // Состояние для хранения видимости карточного меню
const [isLoginMenuVisible, setIsLoginMenuVisible] = useState(false); const [isLoginMenuVisible, setIsLoginMenuVisible] = useState(false); // Состояние для хранения видимости меню входа
const navigate = useNavigate(); const navigate = useNavigate(); // Функция для навигации
const toggleCatalogMenu = () => { const toggleCatalogMenu = () => setIsCatalogMenuVisible(prevState => !prevState); // Функция для переключения видимости карточного меню
setIsCatalogMenuVisible(prevState => !prevState); const toggleLoginMenu = () => setIsLoginMenuVisible(prevState => !prevState); // Функция для переключения видимости меню входа
const handleProfileClick = () => { // Функция для перехода на страницу профиля при нажатии на кнопку
const userCookie = Cookies.get('user'); // Проверка на наличие куки с логином
userCookie ? navigate('/profile') : toggleLoginMenu(); // Переход на страницу профиля если куки есть, иначе переключение видимости меню входа
}; };
const toggleLoginMenu = () => { const resetCategoryFilter = () => onSelectCategory('all'); // Функция для сброса фильтрации категорий
setIsLoginMenuVisible(prevState => !prevState);
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => { // Функция для обработки нажатия клавиши Enter в поле ввода
if (event.key === 'Enter') { // Предотвращение отправки формы при нажатии Enter
event.preventDefault();
}
}; };
const handleProfileClick = () => {
const userCookie = Cookies.get('user');
userCookie ? navigate('/profile') : toggleLoginMenu();
};
const resetCategoryFilter = () => {
onSelectCategory('all');
};
function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
if (event.key === 'Enter') {
event.preventDefault();
}
}
return( return(
<header className="header"> <header className="header">
@@ -67,8 +59,7 @@ export default function Header({ togglePopupMap, onSelectCategory, onSearchChang
<input <input
type="text" type="text"
onChange={(e) => onSearchChange(e.target.value)} onChange={(e) => onSearchChange(e.target.value)}
name="search" name="search"
id=""
className="search-form__input" className="search-form__input"
placeholder="Я ищу..." placeholder="Я ищу..."
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}

View File

@@ -1,103 +1,115 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from 'react';
import { motion } from "framer-motion"; import { motion } from 'framer-motion';
import axios from "axios"; import axios from 'axios';
import { useNavigate } from "react-router-dom"; import { useNavigate } from 'react-router-dom';
import Cookies from "js-cookie"; import Cookies from 'js-cookie';
interface LoginMenuProps { interface LoginMenuProps { // Интерфейс для пропсов компонента LoginMenu
toggleLoginMenu: () => void; toggleLoginMenu: () => void; // Функция для переключения видимости меню входа
} }
export default function LoginMenu({ toggleLoginMenu }: LoginMenuProps): JSX.Element { export default function LoginMenu({ toggleLoginMenu }: LoginMenuProps) {
const [isLoginMode, setIsLoginMode] = useState(true); const [isLoginMode, setIsLoginMode] = useState(true); // Состояние для определения режима входа или регистрации
const [login, setLogin] = useState(''); const [login, setLogin] = useState(''); // Состояние для хранения введенного логина
const [password, setPassword] = useState(''); const [password, setPassword] = useState(''); // Состояние для хранения введенного пароля
const navigate = useNavigate(); const navigate = useNavigate(); // Функция для навигации
const toggleMode = () => { const toggleMode = () => setIsLoginMode(!isLoginMode); // Функция для переключения режима входа или регистрации
setIsLoginMode(!isLoginMode);
}
const handleClose = () => { const handleClose = () => { // Функция для закрытия меню входа
document.body.classList.remove('no-scroll'); document.body.classList.remove('no-scroll'); // Удаление класса "no-scroll" с тела документа
toggleLoginMenu(); toggleLoginMenu(); // Вызов функции переключения видимости меню входа
};
useEffect(() => { // Эффект для добавления класса "no-scroll" с тела документа при монтировании компонента
document.body.classList.add('no-scroll');
return () => {
document.body.classList.remove('no-scroll');
}; };
}, []);
useEffect(() => { const handleAuth = async (isRegistering: boolean) => { // Функция для обработки авторизации
document.body.classList.add('no-scroll'); try {
return () => { let response;
document.body.classList.remove('no-scroll'); if (isRegistering) {
}; response = await axios.get(
}, []); `http://127.0.0.1:8000/api/post/user?login=${encodeURIComponent(login)}&password=${encodeURIComponent(password)}`
);
const handleAuth = async (isRegistering: boolean) => { } else {
try { response = await axios.get(
let response; `http://127.0.0.1:8000/api/get/user?login=${encodeURIComponent(login)}&password=${encodeURIComponent(password)}`
if (isRegistering) { );
// Регистрация пользователя if (response.data.user.length === 0) {
response = await axios.get(`http://127.0.0.1:8000/api/post/user?login=${encodeURIComponent(login)}&password=${encodeURIComponent(password)}`); alert('Пользователь не найден.');
} else { return;
// Вход в систему
response = await axios.get(`http://127.0.0.1:8000/api/get/user?login=${encodeURIComponent(login)}&password=${encodeURIComponent(password)}`);
// Проверка наличия пользователя
if (response.data.user.length === 0) {
alert('Пользователь не найден.');
// Здесь можно выполнить действия в случае отсутствия пользователя, например, вывести сообщение об ошибке
return;
}
}
if (response.status === 200) {
// Создание cookie файла
Cookies.set('user', login, { expires: 1 }); // Cookie на 1 день
Cookies.set('user_id', response.data.user[0].id, { expires: 1 })
// Перенаправление на страницу профиля
navigate('/profile');
toggleLoginMenu(); // Закрытие меню входа
}
} catch (error) {
alert('Ошибка при авторизации: ' + error);
} }
} }
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { if (response.status === 200) {
event.preventDefault(); Cookies.set('user', login, { expires: 1 }); // Установка куки с логином
await handleAuth(!isLoginMode); Cookies.set('user_id', response.data.user[0].id, { expires: 1 }); // Установка куки с ID пользователя
navigate('/profile'); // Переход на страницу профиля
toggleLoginMenu(); // Вызов функции переключения видимости меню входа
}
} catch (error) {
alert('Ошибка при авторизации: ' + error);
} }
};
return(
<> const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { // Функция для обработки отправки формы
<div className="background-blackout" onClick={handleClose}></div> event.preventDefault();
<form className="popup-login" onSubmit={handleSubmit}> await handleAuth(!isLoginMode);
<div className="popup-login__top-container"> };
<div className="top-container__headings-text">
<h5 className="popup-menu__heading"> return (
SusMarket <span>ID</span> <>
</h5> <div className="background-blackout" onClick={handleClose}></div>
<p className="top-container__text"> <form className="popup-login" onSubmit={handleSubmit}>
{isLoginMode ? 'Войдите с SusMarket ID' : 'Зарегистрируйтесь с SusMarket ID'} <div className="popup-login__top-container">
</p> <div className="top-container__headings-text">
</div> <h5 className="popup-menu__heading">
</div> SusMarket <span>ID</span>
<div className="popup-login__inputs-container"> </h5>
<input type="text" name="userName" id="userName" className="popup-login__name-input" placeholder="Логин" value={login} onChange={(e) => setLogin(e.target.value)}/> <p className="top-container__text">
<input type="password" name="userPassword" id="userPassword" className="popup-login__password-input" placeholder="Пароль" value={password} onChange={(e) => setPassword(e.target.value)}/> {isLoginMode ? 'Войдите с SusMarket ID' : 'Зарегистрируйтесь с SusMarket ID'}
</div> </p>
<div className="popup-login__bottom-container"> </div>
<p className="popup-login__prompt-url" onClick={toggleMode}> </div>
{isLoginMode ? 'У вас нет аккаунта? ' : 'У вас есть аккаунт? '} <div className="popup-login__inputs-container">
<u>{isLoginMode ? 'Зарегистрироваться' : 'Войти'}</u> <input
</p> type="text"
<motion.button name="userName"
type="submit" id="userName"
className="popup-login__login-button" className="popup-login__name-input"
whileTap={{scale: 0.98}} placeholder="Логин"
transition={{duration: 0.2, type: "spring"}} value={login}
> onChange={(e) => setLogin(e.target.value)}
{isLoginMode ? 'Войти' : 'Зарегистрироваться'} />
</motion.button> <input
</div> type="password"
</form> name="userPassword"
</> id="userPassword"
) className="popup-login__password-input"
placeholder="Пароль"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div className="popup-login__bottom-container">
<p className="popup-login__prompt-url" onClick={toggleMode}>
{isLoginMode ? 'У вас нет аккаунта? ' : 'У вас есть аккаунт? '}
<u>{isLoginMode ? 'Зарегистрироваться' : 'Войти'}</u>
</p>
<motion.button
type="submit"
className="popup-login__login-button"
whileTap={{ scale: 0.98 }}
transition={{ duration: 0.2, type: 'spring' }}
>
{isLoginMode ? 'Войти' : 'Зарегистрироваться'}
</motion.button>
</div>
</form>
</>
);
} }

View File

@@ -1,71 +1,79 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from 'react';
import { motion } from "framer-motion"; import { motion } from 'framer-motion';
type ButtonState = 1 | 2 | null; type ButtonState = 1 | 2 | null;
interface PopupMapProps { interface PopupMapProps { // Пропсы, которые принимает компонент PopupMap
togglePopupMap: () => void; togglePopupMap: () => void; // Функция для закрытия всплывающего окна
} }
// Компонент, отображающий всплывающее окно с картой
export default function PopupMap({ togglePopupMap }: PopupMapProps) { export default function PopupMap({ togglePopupMap }: PopupMapProps) {
const [selectedButton, setSelectedButton] = useState<ButtonState>(null); const [selectedButton, setSelectedButton] = useState<ButtonState>(null); // Состояние для отслеживания выбранного кнопки
const handleButtonClick = (buttonId: ButtonState) => { const handleButtonClick = (buttonId: ButtonState) => { // Обработчик клика на кнопку
setSelectedButton(buttonId); setSelectedButton(buttonId);
};
const handleClose = () => { // Обработчик закрытия всплывающего окна
document.body.classList.remove('no-scroll'); // Удаление класса "no-scroll" с тела документа
togglePopupMap(); // Вызов функции для закрытия всплывающего окна
};
useEffect(() => { // Эффект для добавления класса "no-scroll" с тела документа при монтировании компонента
document.body.classList.add('no-scroll');
return () => {
document.body.classList.remove('no-scroll');
}; };
}, []);
const handleClose = () => { return (
document.body.classList.remove('no-scroll'); <>
togglePopupMap(); <div className="background-blackout" onClick={handleClose}></div>
}; <div className="popup-map">
<div className="popup-map__menu-div">
useEffect(() => { <div className="menu-div__container-div">
document.body.classList.add('no-scroll'); <div className="menu-div__delivery-div">
return () => { <motion.button
document.body.classList.remove('no-scroll'); className={`delivery-div__delivery-button ${selectedButton === 1 ? 'delivery-div__delivery-button_selected' : ''}`}
}; onClick={() => handleButtonClick(1)}
}, []); whileTap={{ scale: 0.98 }}
transition={{ duration: 0.02, type: 'spring' }}
return( >
<> Самовывоз
<div className="background-blackout" onClick={handleClose}></div> </motion.button>
<div className="popup-map"> <motion.button
<div className="popup-map__menu-div"> className={`delivery-div__delivery-button ${selectedButton === 2 ? 'delivery-div__delivery-button_selected' : ''}`}
<div className="menu-div__container-div"> onClick={() => handleButtonClick(2)}
<div className="menu-div__delivery-div"> whileTap={{ scale: 0.98 }}
<motion.button transition={{ duration: 0.02, type: 'spring' }}
className={`delivery-div__delivery-button ${selectedButton === 1 ? 'delivery-div__delivery-button_selected' : ''}`} >
onClick={() => handleButtonClick(1)} Курьером
whileTap={{scale: 0.98}} </motion.button>
transition={{duration: 0.02, type: "spring"}}
>
Самовывоз
</motion.button>
<motion.button
className={`delivery-div__delivery-button ${selectedButton === 2 ? 'delivery-div__delivery-button_selected' : ''}`}
onClick={() => handleButtonClick(2)}
whileTap={{scale: 0.98}}
transition={{duration: 0.02, type: "spring"}}
>
Курьером
</motion.button>
</div>
<input type="search" name="address-search" id="address-search" placeholder="Искать на карте" className="menu-div__search-input"/>
</div>
<motion.button
className="menu-div__select-button"
whileTap={{scale: 0.98}}
transition={{duration: 0.01, type: "spring"}}
>
Заберу здесь
</motion.button>
</div>
<div className="popup-map__map-div">
<a href="https://yandex.ru/maps/65/novosibirsk/?utm_medium=mapframe&utm_source=maps" style={{color:"#eee", fontSize:"12px", position:"absolute", top:"0px"}}>Новосибирск</a>
<a href="https://yandex.ru/maps/65/novosibirsk/house/ulitsa_titova_14/bEsYfg9iSkEGQFtufXV5cn9lYQ==/?ll=82.882443%2C54.983268&utm_medium=mapframe&utm_source=maps&z=18.59" style={{color:"#eee", fontSize:"12px", position:"absolute", top:"14px"}}>Улица Титова, 14 Яндекс Карты</a>
<iframe title="map" src="https://yandex.ru/map-widget/v1/?ll=82.882443%2C54.983268&mode=search&ol=geo&ouri=ymapsbm1%3A%2F%2Fgeo%3Fdata%3DCgg1NzA5NDgyMhJB0KDQvtGB0YHQuNGPLCDQndC-0LLQvtGB0LjQsdC40YDRgdC6LCDRg9C70LjRhtCwINCi0LjRgtC-0LLQsCwgMTQiCg3Dw6VCFffuW0I%2C&z=18.59" width="100%" height="100%" style={{position:"relative"}} className="popup-map__map-iframe"></iframe>
</div>
</div> </div>
</> <input type="search" name="address-search" id="address-search" placeholder="Искать на карте" className="menu-div__search-input" />
) </div>
<motion.button
className="menu-div__select-button"
whileTap={{ scale: 0.98 }}
transition={{ duration: 0.01, type: 'spring' }}
>
Заберу здесь
</motion.button>
</div>
<div className="popup-map__map-div">
<a href="https://yandex.ru/maps/65/novosibirsk/?utm_medium=mapframe&utm_source=maps" style={{ color: "#eee", fontSize: "12px", position: "absolute", top: "0px" }}>Новосибирск</a>
<a href="https://yandex.ru/maps/65/novosibirsk/house/ulitsa_titova_14/bEsYfg9iSkEGQFtufXV5cn9lYQ==/?ll=82.882443%2C54.983268&utm_medium=mapframe&utm_source=maps&z=18.59" style={{ color: "#eee", fontSize: "12px", position: "absolute", top: "14px" }}>Улица Титова, 14 Яндекс Карты</a>
<iframe
title="map"
src="https://yandex.ru/map-widget/v1/?ll=82.882443%2C54.983268&mode=search&ol=geo&ouri=ymapsbm1%3A%2F%2Fgeo%3Fdata%3DCgg1NzA5NDgyMhJB0KDQvtGB0YHQuNGPLCDQndC-0LLQvtGB0LjQsdC40YDRgdC6LCDRg9C70LjRhtCwINCi0LjRgtC-0LLQsCwgMTQiCg3Dw6VCFffuW0I%2C&z=18.59"
width="100%"
height="100%"
style={{ position: "relative" }}
className="popup-map__map-iframe"
></iframe>
</div>
</div>
</>
);
} }

View File

@@ -8,42 +8,30 @@ type ReviewProps = {
review: Reviews; review: Reviews;
}; };
// Компонент для отображения отзыва
export default function Review({ review }: ReviewProps) { export default function Review({ review }: ReviewProps) {
// Состояние для логина пользователя const [userName, setUserName] = useState<string>(""); // Состояние для имени пользователя
const [userName, setUserName] = useState<string>(""); const readableDate = new Date(review.date).toLocaleDateString('ru-RU'); // Преобразование даты в читабельную форму
// Читаем дату отзыва
const readableDate = new Date(review.date).toLocaleDateString('ru-RU');
// useEffect для получения логина пользователя useEffect(() => { // Получение имени пользователя по его ID
useEffect(() => {
// Запрос к api для получения логина пользователя
axios.get(`http://127.0.0.1:8000/api/get/user/${review.user_id}`) axios.get(`http://127.0.0.1:8000/api/get/user/${review.user_id}`)
.then(response => { .then(response => {
const user = response.data.user[0]; const user = response.data.user[0];
// Устанавливаем логин пользователя setUserName(user.login);
setUserName(user.login); })
}) .catch(error => {
.catch(error => { console.error('Ошибка при получении логина пользователя:', error);
console.error('Ошибка при получении логина пользователя:', error); });
}); }, [review.user_id]);
}, [review.user_id]);
// Возвращаем JSX для отображения отзыва
return ( return (
<article className="review-article"> <article className="review-article">
<div className="review-article__review-container"> <div className="review-article__review-container">
<div className="review-container__user-info"> <div className="review-container__user-info">
{/* Отображаем аватарку пользователя */}
<img className="user-info__user-avatar" src={UserAvatar} alt="Review user avatar" /> <img className="user-info__user-avatar" src={UserAvatar} alt="Review user avatar" />
<h4 className="user-info__user-name"> <h4 className="user-info__user-name">{userName}</h4>
{/* Отображаем логин пользователя */}
{userName}
</h4>
</div> </div>
<div className="review-container__review-info"> <div className="review-container__review-info">
<div className="review-info__star-rate"> <div className="review-info__star-rate">
{/* Отображаем рейтинг отзыва */}
{[1, 2, 3, 4, 5].map(rate => ( {[1, 2, 3, 4, 5].map(rate => (
<input <input
key={rate} key={rate}
@@ -57,18 +45,12 @@ export default function Review({ review }: ReviewProps) {
))} ))}
</div> </div>
<time className="review-info__review-date" dateTime={new Date(review.date).toISOString()}> <time className="review-info__review-date" dateTime={new Date(review.date).toISOString()}>
{/* Отображаем дату отзыва */}
{readableDate} {readableDate}
</time> </time>
</div> </div>
</div> </div>
<p className="review-article__text-p"> <p className="review-article__text-p">{review.commentary}</p>
{/* Отображаем текст отзыва */}
{review.commentary}
</p>
{/* Отображаем изображение товара, если оно есть */}
{review.icons && <img className="review-article__product-image" src={review.icons} alt="Review product" />} {review.icons && <img className="review-article__product-image" src={review.icons} alt="Review product" />}
</article> </article>
); );
} }

View File

@@ -8,30 +8,30 @@ import Cookies from 'js-cookie';
interface ReviewState { interface ReviewState {
text: string; text: string;
rating: number; rating: number;
image?: string | ArrayBuffer | null; // Изображение может быть в формате base64 image?: string | ArrayBuffer | null;
} }
export default function ReviewForm({ productId }: { productId: string }) { export default function ReviewForm({ productId }: { productId: string }) {
const [review, setReview] = useState<ReviewState>({ text: '', rating: 1 }); const [review, setReview] = useState<ReviewState>({ text: '', rating: 1 }); // Состояние для отзыва
const [userId, setUserId] = useState<string | null>(null); const [userId, setUserId] = useState<string | null>(null); // Состояние для ID пользователя
const [imageName, setImageName] = useState<string | null>(null); const [imageName, setImageName] = useState<string | null>(null); // Состояние для имени изображения
useEffect(() => { useEffect(() => { // Получение ID пользователя из cookie при инициализации компонента
const userIdFromCookie = Cookies.get('user_id'); const userIdFromCookie = Cookies.get('user_id');
if (userIdFromCookie) { if (userIdFromCookie) {
setUserId(userIdFromCookie); setUserId(userIdFromCookie);
} }
}, []); }, []);
function handleTextChange(event: React.ChangeEvent<HTMLTextAreaElement>) { function handleTextChange(event: React.ChangeEvent<HTMLTextAreaElement>) { // Обработчик изменения текста отзыва
setReview({ ...review, text: event.target.value }); setReview({ ...review, text: event.target.value });
} }
function handleRatingChange(event: React.ChangeEvent<HTMLInputElement>) { function handleRatingChange(event: React.ChangeEvent<HTMLInputElement>) { // Обработчик изменения оценки отзыва
setReview({ ...review, rating: Number(event.target.value) }); setReview({ ...review, rating: Number(event.target.value) });
} }
function handleImageChange(event: React.ChangeEvent<HTMLInputElement>) { function handleImageChange(event: React.ChangeEvent<HTMLInputElement>) { // Обработчик изменения изображения
if (event.target.files && event.target.files[0]) { if (event.target.files && event.target.files[0]) {
const file = event.target.files[0]; const file = event.target.files[0];
setImageName(file.name); setImageName(file.name);
@@ -44,10 +44,10 @@ export default function ReviewForm({ productId }: { productId: string }) {
} }
} }
async function handleSubmit(event: React.FormEvent) { async function handleSubmit(event: React.FormEvent) { // Обработчик отправки формы
event.preventDefault(); event.preventDefault();
if (!userId) { if (!userId) {
console.error('User ID not found!'); console.error('ID пользователя не найден!');
return; return;
} }
try { try {
@@ -65,19 +65,17 @@ export default function ReviewForm({ productId }: { productId: string }) {
'Content-Type': 'application/x-www-form-urlencoded' 'Content-Type': 'application/x-www-form-urlencoded'
} }
}); });
alert("Отзыв успешно отправлен!") alert("Отзыв успешно отправлен!");
// Опционально: добавить логику для обновления списка отзывов на странице после успешной отправки
} catch (error) { } catch (error) {
console.error('Error submitting review:', error); console.error('Ошибка при отправке отзыва:', error);
} }
} }
return ( return (
<form className='product-page__review-form' onSubmit={handleSubmit}> <form className='product-page__review-form' onSubmit={handleSubmit}>
<h5 className='review-form__heading'> <h5 className='review-form__heading'>Оставить отзыв</h5>
Оставить отзыв
</h5>
<div className="review-form__stars-container"> <div className="review-form__stars-container">
{/* Создание радиокнопок для выбора оценки */}
{[...Array(5)].map((_, index) => ( {[...Array(5)].map((_, index) => (
<input <input
key={index} key={index}

View File

@@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import { StrictMode } from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import './index.scss'; import './index.scss';
@@ -8,9 +7,9 @@ import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render( root.render(
<StrictMode> <React.StrictMode>
<BrowserRouter> <BrowserRouter>
<App /> <App />
</BrowserRouter> </BrowserRouter>
</StrictMode> </React.StrictMode>
); );

View File

@@ -30,5 +30,4 @@ export default function HomePage({ products }: HomePageProps) {
</div> </div>
</section> </section>
); );
} }

View File

@@ -7,7 +7,6 @@ import NoKesspenAvatar from "../assets/img/info-page__no-kesspen-avatar.png";
export default function InfoPage() { export default function InfoPage() {
return ( return (
<section className="info-page"> <section className="info-page">
{/* Компонент DevCard отображает информацию о разработчике */}
<DevCard <DevCard
avatar={NoKesspenAvatar} avatar={NoKesspenAvatar}
name="No_Kesspen" name="No_Kesspen"
@@ -22,4 +21,4 @@ export default function InfoPage() {
/> />
</section> </section>
); );
} }

View File

@@ -2,71 +2,57 @@ import React, { useState } from "react";
import '../PaymentStyle.scss'; import '../PaymentStyle.scss';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
// Компонент страницы оплаты
export default function PaymentPage() { export default function PaymentPage() {
// Состояние номера кредитной карты const [ccNumber, setCcNumber] = useState(""); // Состояние для номера карты
const [ccNumber, setCcNumber] = useState(""); const [valueDate, setValueDate] = useState<number | ''>(''); // Состояние для даты истечения срока действия карты
// Состояние даты истечения срока карты const [valueCode, setValueCode] = useState<number | ''>(''); // Состояние для кода карты
const [valueDate, setValueDate] = useState<number | ''>(''); const location = useLocation(); // Получение параметров из URL
// Состояние кода безопасности карты
const [valueCode, setValueCode] = useState<number | ''>('');
// Получение параметров из URL
const location = useLocation();
const queryParams = new URLSearchParams(location.search); const queryParams = new URLSearchParams(location.search);
// Получение цены из параметров URL const price = queryParams.get('price'); // Получение стоимости из URL
const price = queryParams.get('price');
// Функция для форматирования и установки номера кредитной карты const formatAndSetCcNumber = (e: React.ChangeEvent<HTMLInputElement>) => { // Обработчик изменения номера карты
const formatAndSetCcNumber = (e: React.ChangeEvent<HTMLInputElement>) => { const inputVal = e.target.value.replace(/ /g, ""); // Удаление пробелов из введенного значения
const inputVal = e.target.value.replace(/ /g, ""); let inputNumbersOnly = inputVal.replace(/\D/g, ""); // Удаление всех символов, кроме цифр
let inputNumbersOnly = inputVal.replace(/\D/g, "");
if (inputNumbersOnly.length > 16) { if (inputNumbersOnly.length > 16) { // Если введенное значение превышает 16 символов
inputNumbersOnly = inputNumbersOnly.substr(0, 16); inputNumbersOnly = inputNumbersOnly.substr(0, 16); // Усекаем его до 16 символов
} }
const splits = inputNumbersOnly.match(/.{1,4}/g); const splits = inputNumbersOnly.match(/.{1,4}/g); // Разделяем введенное значение на группы по 4 символа
let spacedNumber = ""; let spacedNumber = ""; // Строка для хранения введенного значения с разделителями
if (splits) { if (splits) { // Если разделение прошло успешно
spacedNumber = splits.join(" "); spacedNumber = splits.join(" "); // Добавляем пробелы между группами по 4 символа
} }
setCcNumber(spacedNumber); setCcNumber(spacedNumber); // Устанавливаем введенное значение в состояние
}; };
// Функция для обработки изменения даты истечения срока карты const handleChangeDate = (event: React.ChangeEvent<HTMLInputElement>) => { // Обработчик изменения даты истечения срока действия карты
const handleChangeDate = (event: React.ChangeEvent<HTMLInputElement>) => { const inputValue = event.target.value; // Получаем введенное значение
const inputValue = event.target.value; if (inputValue.length <= 4) { // Если введенное значение содержит не больше 4 символов
if (inputValue.length <= 4) { setValueDate(inputValue === '' ? '' : Number(inputValue)); // Устанавливаем значение в состояние
setValueDate(inputValue === '' ? '' : Number(inputValue));
} }
}; };
// Функция для обработки изменения кода безопасности карты const handleChangeCode = (event: React.ChangeEvent<HTMLInputElement>) => { // Обработчик изменения кода карты
const handleChangeCode = (event: React.ChangeEvent<HTMLInputElement>) => { const inputValue = event.target.value; // Получаем введенное значение
const inputValue = event.target.value; if (inputValue.length <= 3) { // Если введенное значение содержит не больше 3 символов
if (inputValue.length <= 3) { setValueCode(inputValue === '' ? '' : Number(inputValue)); // Устанавливаем значение в состояние
setValueCode(inputValue === '' ? '' : Number(inputValue));
} }
}; };
return( return(
<section className="payment-page"> <section className="payment-page">
{/* Отображение цены */}
<h2 className="payment-page__price">{price} </h2> <h2 className="payment-page__price">{price} </h2>
<div className="payment-page__payment-card"> <div className="payment-page__payment-card">
<h3 className="payment-card__heading"> Оплата картой </h3> <h3 className="payment-card__heading"> Оплата картой </h3>
{/* Ввод номера кредитной карты */}
<input className="payment-card__input" type="text" placeholder="Номер" value={ccNumber} onChange={formatAndSetCcNumber}/> <input className="payment-card__input" type="text" placeholder="Номер" value={ccNumber} onChange={formatAndSetCcNumber}/>
<div className="payment-card__inputs-group"> <div className="payment-card__inputs-group">
{/* Ввод даты истечения срока карты */}
<input className="payment-card__input" type="number" placeholder="ММ/ГГ" value={valueDate} onChange={handleChangeDate}/> <input className="payment-card__input" type="number" placeholder="ММ/ГГ" value={valueDate} onChange={handleChangeDate}/>
{/* Ввод кода безопасности карты */}
<input className="payment-card__input" type="number" placeholder="CVC/CVV" value={valueCode} onChange={handleChangeCode}/> <input className="payment-card__input" type="number" placeholder="CVC/CVV" value={valueCode} onChange={handleChangeCode}/>
</div> </div>
</div> </div>
{/* Кнопка для оплаты */}
<a href="scam" className="payment-page__pay-link"> Оплатить </a> <a href="scam" className="payment-page__pay-link"> Оплатить </a>
</section> </section>
) )

View File

@@ -1,4 +1,4 @@
import React, {useState, useEffect} from 'react'; import React, { useState, useEffect } from 'react';
import { Product, Reviews } from '../utils/types'; import { Product, Reviews } from '../utils/types';
import Review from '../components/Review'; import Review from '../components/Review';
import axios from 'axios'; import axios from 'axios';
@@ -8,150 +8,135 @@ import ReviewForm from '../components/ReviewForm';
import { Link, useParams } from 'react-router-dom'; import { Link, useParams } from 'react-router-dom';
export default function ProductPage() { export default function ProductPage() {
function trimText(text: string, limit: number) { //сокращение описания const { id } = useParams(); // Получение id из URL-параметров
return text.length > limit ? text.substring(0, limit) + '...' : text;
}
const { id } = useParams(); //возвращает id товара из Url // Состояние для продукта и рецензий
const [product, setProduct] = useState<Product | null>(null); //состояние для данных о товаре const [product, setProduct] = useState<Product | null>(null);
const [reviews, setReviews] = useState<Reviews[]>([]); //состаяние для отзывов const [reviews, setReviews] = useState<Reviews[]>([]);
const [averageRating, setAverageRating] = useState<number>(0); //состояние для средней арифметической оценки товара
const [isDataFetched, setIsDataFetched] = useState(false); //состояние для отслеживания кэширования данных из запроса // Состояние для среднего рейтинга и флага для отслеживания получения данных
const totalReviews = reviews.length; //количество отзывов const [averageRating, setAverageRating] = useState<number>(0);
const countReviewsByRate = (rate: number): number => { //подсчёт отзывов с данной оценкой const [isDataFetched, setIsDataFetched] = useState(false);
const totalReviews = reviews.length; // Количество рецензий
const trimText = (text: string, limit: number): string => { // Функция для усечения текста
return text.length > limit ? text.substring(0, limit) + '...' : text;
};
const countReviewsByRate = (rate: number): number => { // Функция для подсчета рецензий по рейтингу
return reviews.filter(review => review.rate === rate).length; return reviews.filter(review => review.rate === rate).length;
}; };
const percentageOfRate = (rate: number): number => { //расчёт процента отзывов с данной оценкой от количетсва всех отзывов
const percentageOfRate = (rate: number): number => { // Функция для вычисления процента рецензий по рейтингу
const count = countReviewsByRate(rate); const count = countReviewsByRate(rate);
return (count / totalReviews) * 100; return (count / totalReviews) * 100;
}; };
useEffect(() => { //запрос к api данных о товаре useEffect(() => { // Получение продукта по его id
axios axios.get('http://127.0.0.1:8000/api/get/products')
.get('http://127.0.0.1:8000/api/get/products')
.then(response => { .then(response => {
const productData = response.data.products.find( //"извлечение" данных о товаре из массива по его id const productData = response.data.products.find(
(item: Product) => item.id.toString() === id (item: Product) => item.id.toString() === id
); );
setProduct(productData); setProduct(productData);
}) })
.catch(error => { .catch(error => {
console.error('There was an error fetching the products', error); console.error('Ошибка при получении продукта:', error);
}); });
}, [id]); }, [id]);
useEffect(() => { //запрос к api отзывов у товара useEffect(() => { // Получение рецензий по id продукта
if (!isDataFetched) { //проверка на кэширование данных if (!isDataFetched) {
axios axios.get(`http://127.0.0.1:8000/api/get/reviews/${id}`)
.get(`http://127.0.0.1:8000/api/get/reviews/${id}`)
.then(response => { .then(response => {
setReviews(response.data.review); const reviewsData = response.data.review;
const totalRating = response.data.review.reduce((acc: number, review: Reviews) => acc + review.rate, 0); //общий рейтинг отзывов setReviews(reviewsData);
const average = totalRating / response.data.review.length; //средннее арифметический рейтинг всех отзывов const totalRating = reviewsData.reduce((acc: number, review: Reviews) => acc + review.rate, 0);
if (response.data.review.length > 0) { //проверка на наличие отзывов const average = totalRating / reviewsData.length;
setAverageRating(average); setAverageRating(reviewsData.length > 0 ? average : 0);
}
else {
setAverageRating(0);
}
setIsDataFetched(true); setIsDataFetched(true);
}) })
.catch(error => { .catch(error => {
console.error('There was an error fetching the reviews', error) console.error('Ошибка при получении рецензий:', error);
}) });
} }
}, [id, isDataFetched]) }, [id, isDataFetched]);
if (!product) { if (!product) { // Отображение загрузки, если продукт не загружен
return <div>Loading</div>; return <div>Загрузка...</div>;
} }
return( return (
<section className="product-page"> <section className="product-page">
<section className="product-page__main-section"> <section className="product-page__main-section">
<img src={product.icons} className="product-page__img" alt="изображение товара"/> <img src={product.icons} className="product-page__img" alt="Product" />
<div className="product-page__info-div"> <div className="product-page__info-div">
<span className="product-page__text-span"> <span className="product-page__text-span">
<h2 className="product-page__heading-h2"> <h2 className="product-page__heading-h2">{product.title}</h2>
{product.title} <p className="product-page__short-desc-div">{trimText(product.description, 200)}</p>
</h2>
<p className="product-page__short-desc-div">
{trimText(product.description, 200)}
</p>
</span> </span>
<div className="product-page__container-div"> <div className="product-page__container-div">
<button className="product-page__share-button"> <button className="product-page__share-button">
<img src={ShareIcon as unknown as string} alt="" /> <img src={ShareIcon} alt="Share" />
</button> </button>
<div className="product-page__price-buy-div"> <div className="product-page__price-buy-div">
<span className="product-page__price-span"> <span className="product-page__price-span">{product.price} </span>
{product.price} <Link to={`/payment?price=${product.price}`} className="product-page__buy-link">Купить</Link>
</span>
<Link to={`/payment?price=${product.price}`} className="product-page__buy-link">
Купить
</Link>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<section className="product-page__info-section"> <section className="product-page__info-section">
<h3 className="product-page__block-heading"> <h3 className="product-page__block-heading">Описание</h3>
Описание <p className="product-page__desc">{product.description}</p>
</h3>
<p className="product-page__desc">
{product.description}
</p>
<ul className="product-page__tags-ul"> <ul className="product-page__tags-ul">
{(product.tags.split('|')).map((tag, index) => ( //разделение тегов {product.tags.split('|').map((tag, index) => (
<li key={index} className="product-page__tag-li"> <li key={index} className="product-page__tag-li">{tag}</li>
{tag}
</li>
))} ))}
</ul> </ul>
</section> </section>
<section className="product-page__reviews-section"> <section className="product-page__reviews-section">
<h3 className="product-page__block-heading"> <h3 className="product-page__block-heading">Отзывы</h3>
Отзывы
</h3>
<div className='reviews-section__rate-block'> <div className='reviews-section__rate-block'>
<div className='rate-block__rating'> <div className='rate-block__rating'>
<span className='rate-block__rate-number'> <span className='rate-block__rate-number'>{averageRating.toFixed(1)}</span>
{averageRating.toFixed(1)}
</span>
<div className="rate-block__star-rating"> <div className="rate-block__star-rating">
{/* Контейнер для отображения звезд, занимающий 100% ширины */}
<div className="star-rating__back-stars"> <div className="star-rating__back-stars">
{'★★★★★'.split('').map((star, i) => ( //пожалуйста, не спрашивайте как это работает {/* Отображение звезд, которые не должны быть закрашены */}
<span key={`back-star-${i}`}>{star}</span>
))}
<div className="star-rating__front-stars" style={{ width: `${(averageRating / 5) * 100}%` }}>
{'★★★★★'.split('').map((star, i) => ( {'★★★★★'.split('').map((star, i) => (
<span key={`front-star-${i}`}>{star}</span> <span key={`back-star-${i}`}>{star}</span>
))} ))}
{/* Контейнер для отображения звезд, которые должны быть закрашены */}
<div className="star-rating__front-stars"
style={{ width: `${(averageRating / 5) * 100}%` }}>
{/* Отображение звезд, которые должны быть закрашены */}
{'★★★★★'.split('').map((star, i) => (
<span key={`front-star-${i}`}>{star}</span>
))}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div className='rate-block__progressbars-group'> <div className='rate-block__progressbars-group'>
{[5, 4, 3, 2, 1].map(rate => ( {[5, 4, 3, 2, 1].map(rate => (
<div className='progressbars-group__progressbar-container' key={rate}> <div className='progressbars-group__progressbar-container' key={rate}>
<span className='rate-progressbar__rate-number'> <span className='rate-progressbar__rate-number'>{rate}</span>
{rate} <div className='progressbar-container__progressbar'>
</span> <div className='progressbar__active-line' style={{ width: `${percentageOfRate(rate)}%` }}></div>
<div className='progressbar-container__progressbar'> </div>
<div className='progressbar__active-line' style={{ width: `${percentageOfRate(rate)}%` }}></div> </div>
</div> ))}
</div>
))}
</div> </div>
</div> </div>
<ReviewForm productId={product.id.toLocaleString('ru-RU')}/> <ReviewForm productId={product.id.toLocaleString('ru-RU')} />
<div className='product-page__reviews-container'> <div className='product-page__reviews-container'>
{reviews.map((review) => ( {reviews.map(review => (
<Review key={review.id} review={review} /> <Review key={review.id} review={review} />
))} ))}
</div> </div>
</section> </section>
</section> </section>
) );
} }