mirror of
https://github.com/yawaflua/SusMarket.git
synced 2025-12-08 19:49:36 +02:00
Commenting code and bringing it to the same style
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"files": {
|
||||
"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/info-page__railth-avatar.png": "/static/media/info-page__railth-avatar.cbf11c43b5ef243b38c0.png",
|
||||
"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",
|
||||
"index.html": "/index.html",
|
||||
"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": [
|
||||
"static/css/main.1aa814f6.css",
|
||||
"static/js/main.47a170f7.js"
|
||||
"static/js/main.0ec10cd6.js"
|
||||
]
|
||||
}
|
||||
@@ -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
1
reactapp/build/static/js/main.0ec10cd6.js.map
Normal file
1
reactapp/build/static/js/main.0ec10cd6.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -12,92 +12,67 @@ import PopupMap from "./components/PopupMap";
|
||||
import { Product, Category } from "./utils/types";
|
||||
|
||||
interface AppPopupMapState {
|
||||
isPopupMapVisible: boolean;
|
||||
isPopupMapVisible: boolean;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
// Состояние для отображения/скрытия карты
|
||||
const [state, setState] = useState<AppPopupMapState>({
|
||||
isPopupMapVisible: false,
|
||||
});
|
||||
const [state, setState] = useState<AppPopupMapState>({ isPopupMapVisible: false });
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [selectedCategory, setSelectedCategory] = useState<Category | 'all'>('all');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// Массив товаров
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
// Выбранная категория или все категории
|
||||
const [selectedCategory, setSelectedCategory] = useState<Category | 'all'>('all');
|
||||
// Поисковый запрос
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
useEffect(() => {
|
||||
axios.get('http://127.0.0.1:8000/api/get/products')
|
||||
.then(response => {
|
||||
setProducts(response.data.products);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching the products:', error);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Получение товаров при загрузке компонента
|
||||
useEffect(() => {
|
||||
axios.get('http://127.0.0.1:8000/api/get/products')
|
||||
.then(response => {
|
||||
setProducts(response.data.products);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('There was an error fetching the products', error);
|
||||
});
|
||||
}, []);
|
||||
const togglePopupMap = () => {
|
||||
setState(prevState => {
|
||||
if (!prevState.isPopupMapVisible) {
|
||||
document.body.classList.add('no-scroll');
|
||||
} else {
|
||||
document.body.classList.remove('no-scroll');
|
||||
}
|
||||
return { ...prevState, isPopupMapVisible: !prevState.isPopupMapVisible };
|
||||
});
|
||||
};
|
||||
|
||||
// Функция для переключения отображения/скрытия карты
|
||||
const togglePopupMap = () => {
|
||||
setState((prevState) => {
|
||||
if (!prevState.isPopupMapVisible) {
|
||||
document.body.classList.add('no-scroll');
|
||||
} else {
|
||||
document.body.classList.remove('no-scroll');
|
||||
}
|
||||
return {
|
||||
...prevState,
|
||||
isPopupMapVisible: !prevState.isPopupMapVisible,
|
||||
};
|
||||
});
|
||||
};
|
||||
const handleSearchChange = (query: string) => {
|
||||
setSearchQuery(query);
|
||||
};
|
||||
|
||||
// Обработчик изменения поискового запроса
|
||||
const handleSearchChange = (query: string) => {
|
||||
setSearchQuery(query);
|
||||
};
|
||||
const filteredProducts = products.filter(product =>
|
||||
(selectedCategory === 'all' || product.category_id === selectedCategory.id) &&
|
||||
product.title.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
// Фильтрация продуктов по выбранной категории и поисковому запросу
|
||||
const filteredProducts = products.filter(product =>
|
||||
(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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,44 +1,46 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
import { Category } from "../utils/types";
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import { Category } from '../utils/types';
|
||||
|
||||
interface CatalogMenuProps {
|
||||
toggleCatalogMenu: () => void;
|
||||
onSelectCategory: (category: Category | 'all') => void;
|
||||
interface CatalogMenuProps { // Пропсы, которые компонент принимает
|
||||
toggleCatalogMenu: () => void; // Функция для закрытия меню
|
||||
onSelectCategory: (category: Category | 'all') => void; // Функция для выбора категории
|
||||
}
|
||||
|
||||
export default function CatalogMenu({ toggleCatalogMenu, onSelectCategory }: CatalogMenuProps): JSX.Element {
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
export default function CatalogMenu({ toggleCatalogMenu, onSelectCategory }: CatalogMenuProps) {
|
||||
const [categories, setCategories] = useState<Category[]>([]); // Состояние для хранения категорий
|
||||
|
||||
useEffect(() => {
|
||||
axios.get('http://127.0.0.1:8000/api/get/category')
|
||||
.then(response => {
|
||||
setCategories(response.data.categories);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`There was an error retrieving the data: ${error}`);
|
||||
});
|
||||
}, []);
|
||||
useEffect(() => { // При монтировании компонента запрашиваем категории с сервера
|
||||
axios.get('http://127.0.0.1:8000/api/get/category')
|
||||
.then(response => {
|
||||
setCategories(response.data.categories);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`Ошибка при получении данных: ${error}`); // Обрабатываем ошибку
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="background-blackout" onClick={toggleCatalogMenu}></div>
|
||||
<ul className="catalog-menu">
|
||||
{categories.map((category) => (
|
||||
<li
|
||||
key={category.id}
|
||||
className="catalog-menu__point-li"
|
||||
onClick={() => onSelectCategory(category)}
|
||||
>
|
||||
<img
|
||||
className="catalog-menu__category-icon"
|
||||
src={category.image}
|
||||
alt={category.title}
|
||||
/>
|
||||
{category.title}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{/* Задний фон для закрытия меню */}
|
||||
<div className="background-blackout" onClick={toggleCatalogMenu}></div>
|
||||
{/* Список категорий */}
|
||||
<ul className="catalog-menu">
|
||||
{categories.map((category) => (
|
||||
<li
|
||||
key={category.id}
|
||||
className="catalog-menu__point-li"
|
||||
onClick={() => onSelectCategory(category)} // Выбор категории
|
||||
>
|
||||
<img
|
||||
className="catalog-menu__category-icon"
|
||||
src={category.image}
|
||||
alt={category.title}
|
||||
/>
|
||||
{category.title}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,48 +1,40 @@
|
||||
import React, { useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Link } from "react-router-dom";
|
||||
import Logotype from "../assets/img/amongasik.png";
|
||||
import CatalogMenu from "./CatalogMenu";
|
||||
import LoginMenu from "./LoginMenu";
|
||||
import { Category } from "../utils/types";
|
||||
import Cookies from "js-cookie";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import React, { useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import Logotype from '../assets/img/amongasik.png';
|
||||
import CatalogMenu from './CatalogMenu';
|
||||
import LoginMenu from './LoginMenu';
|
||||
import { Category } from '../utils/types';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
interface HeaderProps {
|
||||
togglePopupMap: () => void;
|
||||
onSelectCategory: (category: Category | 'all') => void;
|
||||
onSearchChange: (query: string) => void;
|
||||
interface HeaderProps { // Интерфейс для пропсов компонента Header
|
||||
togglePopupMap: () => void; // Функция для переключения видимости карты
|
||||
onSelectCategory: (category: Category | 'all') => void; // Функция для выбора категории
|
||||
onSearchChange: (query: string) => void; // Функция для изменения строки поиска
|
||||
}
|
||||
|
||||
const MotionLink = motion(Link);
|
||||
const MotionLink = motion(Link); // Вынесение компонента в отдельную переменную для удобства использования
|
||||
|
||||
export default function Header({ togglePopupMap, onSelectCategory, onSearchChange }: HeaderProps): JSX.Element {
|
||||
const [isCatalogMenuVisible, setIsCatalogMenuVisible] = useState(false);
|
||||
const [isLoginMenuVisible, setIsLoginMenuVisible] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const toggleCatalogMenu = () => {
|
||||
setIsCatalogMenuVisible(prevState => !prevState);
|
||||
export default function Header({ togglePopupMap, onSelectCategory, onSearchChange }: HeaderProps) {
|
||||
const [isCatalogMenuVisible, setIsCatalogMenuVisible] = useState(false); // Состояние для хранения видимости карточного меню
|
||||
const [isLoginMenuVisible, setIsLoginMenuVisible] = useState(false); // Состояние для хранения видимости меню входа
|
||||
const navigate = useNavigate(); // Функция для навигации
|
||||
|
||||
const toggleCatalogMenu = () => setIsCatalogMenuVisible(prevState => !prevState); // Функция для переключения видимости карточного меню
|
||||
const toggleLoginMenu = () => setIsLoginMenuVisible(prevState => !prevState); // Функция для переключения видимости меню входа
|
||||
|
||||
const handleProfileClick = () => { // Функция для перехода на страницу профиля при нажатии на кнопку
|
||||
const userCookie = Cookies.get('user'); // Проверка на наличие куки с логином
|
||||
userCookie ? navigate('/profile') : toggleLoginMenu(); // Переход на страницу профиля если куки есть, иначе переключение видимости меню входа
|
||||
};
|
||||
|
||||
const toggleLoginMenu = () => {
|
||||
setIsLoginMenuVisible(prevState => !prevState);
|
||||
|
||||
const resetCategoryFilter = () => onSelectCategory('all'); // Функция для сброса фильтрации категорий
|
||||
|
||||
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(
|
||||
<header className="header">
|
||||
@@ -67,8 +59,7 @@ export default function Header({ togglePopupMap, onSelectCategory, onSearchChang
|
||||
<input
|
||||
type="text"
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
name="search"
|
||||
id=""
|
||||
name="search"
|
||||
className="search-form__input"
|
||||
placeholder="Я ищу..."
|
||||
onKeyDown={handleKeyDown}
|
||||
|
||||
@@ -1,103 +1,115 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import axios from "axios";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Cookies from "js-cookie";
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import axios from 'axios';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
interface LoginMenuProps {
|
||||
toggleLoginMenu: () => void;
|
||||
interface LoginMenuProps { // Интерфейс для пропсов компонента LoginMenu
|
||||
toggleLoginMenu: () => void; // Функция для переключения видимости меню входа
|
||||
}
|
||||
|
||||
export default function LoginMenu({ toggleLoginMenu }: LoginMenuProps): JSX.Element {
|
||||
const [isLoginMode, setIsLoginMode] = useState(true);
|
||||
const [login, setLogin] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const navigate = useNavigate();
|
||||
export default function LoginMenu({ toggleLoginMenu }: LoginMenuProps) {
|
||||
const [isLoginMode, setIsLoginMode] = useState(true); // Состояние для определения режима входа или регистрации
|
||||
const [login, setLogin] = useState(''); // Состояние для хранения введенного логина
|
||||
const [password, setPassword] = useState(''); // Состояние для хранения введенного пароля
|
||||
const navigate = useNavigate(); // Функция для навигации
|
||||
|
||||
const toggleMode = () => {
|
||||
setIsLoginMode(!isLoginMode);
|
||||
}
|
||||
const toggleMode = () => setIsLoginMode(!isLoginMode); // Функция для переключения режима входа или регистрации
|
||||
|
||||
const handleClose = () => {
|
||||
document.body.classList.remove('no-scroll');
|
||||
toggleLoginMenu();
|
||||
const handleClose = () => { // Функция для закрытия меню входа
|
||||
document.body.classList.remove('no-scroll'); // Удаление класса "no-scroll" с тела документа
|
||||
toggleLoginMenu(); // Вызов функции переключения видимости меню входа
|
||||
};
|
||||
|
||||
useEffect(() => { // Эффект для добавления класса "no-scroll" с тела документа при монтировании компонента
|
||||
document.body.classList.add('no-scroll');
|
||||
return () => {
|
||||
document.body.classList.remove('no-scroll');
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.body.classList.add('no-scroll');
|
||||
return () => {
|
||||
document.body.classList.remove('no-scroll');
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleAuth = async (isRegistering: boolean) => {
|
||||
try {
|
||||
let response;
|
||||
if (isRegistering) {
|
||||
// Регистрация пользователя
|
||||
response = await axios.get(`http://127.0.0.1:8000/api/post/user?login=${encodeURIComponent(login)}&password=${encodeURIComponent(password)}`);
|
||||
} else {
|
||||
// Вход в систему
|
||||
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 handleAuth = async (isRegistering: boolean) => { // Функция для обработки авторизации
|
||||
try {
|
||||
let response;
|
||||
if (isRegistering) {
|
||||
response = await axios.get(
|
||||
`http://127.0.0.1:8000/api/post/user?login=${encodeURIComponent(login)}&password=${encodeURIComponent(password)}`
|
||||
);
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
await handleAuth(!isLoginMode);
|
||||
if (response.status === 200) {
|
||||
Cookies.set('user', login, { expires: 1 }); // Установка куки с логином
|
||||
Cookies.set('user_id', response.data.user[0].id, { expires: 1 }); // Установка куки с ID пользователя
|
||||
navigate('/profile'); // Переход на страницу профиля
|
||||
toggleLoginMenu(); // Вызов функции переключения видимости меню входа
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Ошибка при авторизации: ' + error);
|
||||
}
|
||||
|
||||
return(
|
||||
<>
|
||||
<div className="background-blackout" onClick={handleClose}></div>
|
||||
<form className="popup-login" onSubmit={handleSubmit}>
|
||||
<div className="popup-login__top-container">
|
||||
<div className="top-container__headings-text">
|
||||
<h5 className="popup-menu__heading">
|
||||
SusMarket <span>ID</span>
|
||||
</h5>
|
||||
<p className="top-container__text">
|
||||
{isLoginMode ? 'Войдите с SusMarket ID' : 'Зарегистрируйтесь с SusMarket ID'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="popup-login__inputs-container">
|
||||
<input type="text" name="userName" id="userName" className="popup-login__name-input" placeholder="Логин" value={login} onChange={(e) => setLogin(e.target.value)}/>
|
||||
<input type="password" 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>
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { // Функция для обработки отправки формы
|
||||
event.preventDefault();
|
||||
await handleAuth(!isLoginMode);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="background-blackout" onClick={handleClose}></div>
|
||||
<form className="popup-login" onSubmit={handleSubmit}>
|
||||
<div className="popup-login__top-container">
|
||||
<div className="top-container__headings-text">
|
||||
<h5 className="popup-menu__heading">
|
||||
SusMarket <span>ID</span>
|
||||
</h5>
|
||||
<p className="top-container__text">
|
||||
{isLoginMode ? 'Войдите с SusMarket ID' : 'Зарегистрируйтесь с SusMarket ID'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="popup-login__inputs-container">
|
||||
<input
|
||||
type="text"
|
||||
name="userName"
|
||||
id="userName"
|
||||
className="popup-login__name-input"
|
||||
placeholder="Логин"
|
||||
value={login}
|
||||
onChange={(e) => setLogin(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,71 +1,79 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
type ButtonState = 1 | 2 | null;
|
||||
|
||||
interface PopupMapProps {
|
||||
togglePopupMap: () => void;
|
||||
interface PopupMapProps { // Пропсы, которые принимает компонент PopupMap
|
||||
togglePopupMap: () => void; // Функция для закрытия всплывающего окна
|
||||
}
|
||||
|
||||
// Компонент, отображающий всплывающее окно с картой
|
||||
export default function PopupMap({ togglePopupMap }: PopupMapProps) {
|
||||
const [selectedButton, setSelectedButton] = useState<ButtonState>(null);
|
||||
const [selectedButton, setSelectedButton] = useState<ButtonState>(null); // Состояние для отслеживания выбранного кнопки
|
||||
|
||||
const handleButtonClick = (buttonId: ButtonState) => {
|
||||
setSelectedButton(buttonId);
|
||||
const handleButtonClick = (buttonId: ButtonState) => { // Обработчик клика на кнопку
|
||||
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 = () => {
|
||||
document.body.classList.remove('no-scroll');
|
||||
togglePopupMap();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.body.classList.add('no-scroll');
|
||||
return () => {
|
||||
document.body.classList.remove('no-scroll');
|
||||
};
|
||||
}, []);
|
||||
|
||||
return(
|
||||
<>
|
||||
<div className="background-blackout" onClick={handleClose}></div>
|
||||
<div className="popup-map">
|
||||
<div className="popup-map__menu-div">
|
||||
<div className="menu-div__container-div">
|
||||
<div className="menu-div__delivery-div">
|
||||
<motion.button
|
||||
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"}}
|
||||
>
|
||||
Самовывоз
|
||||
</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>
|
||||
return (
|
||||
<>
|
||||
<div className="background-blackout" onClick={handleClose}></div>
|
||||
<div className="popup-map">
|
||||
<div className="popup-map__menu-div">
|
||||
<div className="menu-div__container-div">
|
||||
<div className="menu-div__delivery-div">
|
||||
<motion.button
|
||||
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' }}
|
||||
>
|
||||
Самовывоз
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -8,42 +8,30 @@ type ReviewProps = {
|
||||
review: Reviews;
|
||||
};
|
||||
|
||||
// Компонент для отображения отзыва
|
||||
export default function Review({ review }: ReviewProps) {
|
||||
// Состояние для логина пользователя
|
||||
const [userName, setUserName] = useState<string>("");
|
||||
// Читаем дату отзыва
|
||||
const readableDate = new Date(review.date).toLocaleDateString('ru-RU');
|
||||
const [userName, setUserName] = useState<string>(""); // Состояние для имени пользователя
|
||||
const readableDate = new Date(review.date).toLocaleDateString('ru-RU'); // Преобразование даты в читабельную форму
|
||||
|
||||
// useEffect для получения логина пользователя
|
||||
useEffect(() => {
|
||||
// Запрос к api для получения логина пользователя
|
||||
useEffect(() => { // Получение имени пользователя по его ID
|
||||
axios.get(`http://127.0.0.1:8000/api/get/user/${review.user_id}`)
|
||||
.then(response => {
|
||||
const user = response.data.user[0];
|
||||
// Устанавливаем логин пользователя
|
||||
setUserName(user.login);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Ошибка при получении логина пользователя:', error);
|
||||
});
|
||||
}, [review.user_id]);
|
||||
.then(response => {
|
||||
const user = response.data.user[0];
|
||||
setUserName(user.login);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Ошибка при получении логина пользователя:', error);
|
||||
});
|
||||
}, [review.user_id]);
|
||||
|
||||
// Возвращаем JSX для отображения отзыва
|
||||
return (
|
||||
<article className="review-article">
|
||||
<div className="review-article__review-container">
|
||||
<div className="review-container__user-info">
|
||||
{/* Отображаем аватарку пользователя */}
|
||||
<img className="user-info__user-avatar" src={UserAvatar} alt="Review user avatar" />
|
||||
<h4 className="user-info__user-name">
|
||||
{/* Отображаем логин пользователя */}
|
||||
{userName}
|
||||
</h4>
|
||||
<h4 className="user-info__user-name">{userName}</h4>
|
||||
</div>
|
||||
<div className="review-container__review-info">
|
||||
<div className="review-info__star-rate">
|
||||
{/* Отображаем рейтинг отзыва */}
|
||||
{[1, 2, 3, 4, 5].map(rate => (
|
||||
<input
|
||||
key={rate}
|
||||
@@ -57,18 +45,12 @@ export default function Review({ review }: ReviewProps) {
|
||||
))}
|
||||
</div>
|
||||
<time className="review-info__review-date" dateTime={new Date(review.date).toISOString()}>
|
||||
{/* Отображаем дату отзыва */}
|
||||
{readableDate}
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
<p className="review-article__text-p">
|
||||
{/* Отображаем текст отзыва */}
|
||||
{review.commentary}
|
||||
</p>
|
||||
{/* Отображаем изображение товара, если оно есть */}
|
||||
<p className="review-article__text-p">{review.commentary}</p>
|
||||
{review.icons && <img className="review-article__product-image" src={review.icons} alt="Review product" />}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,30 +8,30 @@ import Cookies from 'js-cookie';
|
||||
interface ReviewState {
|
||||
text: string;
|
||||
rating: number;
|
||||
image?: string | ArrayBuffer | null; // Изображение может быть в формате base64
|
||||
image?: string | ArrayBuffer | null;
|
||||
}
|
||||
|
||||
export default function ReviewForm({ productId }: { productId: string }) {
|
||||
const [review, setReview] = useState<ReviewState>({ text: '', rating: 1 });
|
||||
const [userId, setUserId] = useState<string | null>(null);
|
||||
const [imageName, setImageName] = useState<string | null>(null);
|
||||
const [review, setReview] = useState<ReviewState>({ text: '', rating: 1 }); // Состояние для отзыва
|
||||
const [userId, setUserId] = useState<string | null>(null); // Состояние для ID пользователя
|
||||
const [imageName, setImageName] = useState<string | null>(null); // Состояние для имени изображения
|
||||
|
||||
useEffect(() => {
|
||||
useEffect(() => { // Получение ID пользователя из cookie при инициализации компонента
|
||||
const userIdFromCookie = Cookies.get('user_id');
|
||||
if (userIdFromCookie) {
|
||||
setUserId(userIdFromCookie);
|
||||
}
|
||||
}, []);
|
||||
|
||||
function handleTextChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
|
||||
function handleTextChange(event: React.ChangeEvent<HTMLTextAreaElement>) { // Обработчик изменения текста отзыва
|
||||
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) });
|
||||
}
|
||||
|
||||
function handleImageChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
function handleImageChange(event: React.ChangeEvent<HTMLInputElement>) { // Обработчик изменения изображения
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
const file = event.target.files[0];
|
||||
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();
|
||||
if (!userId) {
|
||||
console.error('User ID not found!');
|
||||
console.error('ID пользователя не найден!');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -65,19 +65,17 @@ export default function ReviewForm({ productId }: { productId: string }) {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
});
|
||||
alert("Отзыв успешно отправлен!")
|
||||
// Опционально: добавить логику для обновления списка отзывов на странице после успешной отправки
|
||||
alert("Отзыв успешно отправлен!");
|
||||
} catch (error) {
|
||||
console.error('Error submitting review:', error);
|
||||
console.error('Ошибка при отправке отзыва:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form className='product-page__review-form' onSubmit={handleSubmit}>
|
||||
<h5 className='review-form__heading'>
|
||||
Оставить отзыв
|
||||
</h5>
|
||||
<h5 className='review-form__heading'>Оставить отзыв</h5>
|
||||
<div className="review-form__stars-container">
|
||||
{/* Создание радиокнопок для выбора оценки */}
|
||||
{[...Array(5)].map((_, index) => (
|
||||
<input
|
||||
key={index}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import { StrictMode } from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import './index.scss';
|
||||
@@ -8,9 +7,9 @@ import App from './App';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</StrictMode>
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -30,5 +30,4 @@ export default function HomePage({ products }: HomePageProps) {
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import NoKesspenAvatar from "../assets/img/info-page__no-kesspen-avatar.png";
|
||||
export default function InfoPage() {
|
||||
return (
|
||||
<section className="info-page">
|
||||
{/* Компонент DevCard отображает информацию о разработчике */}
|
||||
<DevCard
|
||||
avatar={NoKesspenAvatar}
|
||||
name="No_Kesspen"
|
||||
@@ -22,4 +21,4 @@ export default function InfoPage() {
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,71 +2,57 @@ import React, { useState } from "react";
|
||||
import '../PaymentStyle.scss';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
// Компонент страницы оплаты
|
||||
export default function PaymentPage() {
|
||||
// Состояние номера кредитной карты
|
||||
const [ccNumber, setCcNumber] = useState("");
|
||||
// Состояние даты истечения срока карты
|
||||
const [valueDate, setValueDate] = useState<number | ''>('');
|
||||
// Состояние кода безопасности карты
|
||||
const [valueCode, setValueCode] = useState<number | ''>('');
|
||||
// Получение параметров из URL
|
||||
const location = useLocation();
|
||||
const [ccNumber, setCcNumber] = useState(""); // Состояние для номера карты
|
||||
const [valueDate, setValueDate] = useState<number | ''>(''); // Состояние для даты истечения срока действия карты
|
||||
const [valueCode, setValueCode] = useState<number | ''>(''); // Состояние для кода карты
|
||||
const location = useLocation(); // Получение параметров из URL
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
// Получение цены из параметров URL
|
||||
const price = queryParams.get('price');
|
||||
const price = queryParams.get('price'); // Получение стоимости из URL
|
||||
|
||||
// Функция для форматирования и установки номера кредитной карты
|
||||
const formatAndSetCcNumber = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const inputVal = e.target.value.replace(/ /g, "");
|
||||
let inputNumbersOnly = inputVal.replace(/\D/g, "");
|
||||
const formatAndSetCcNumber = (e: React.ChangeEvent<HTMLInputElement>) => { // Обработчик изменения номера карты
|
||||
const inputVal = e.target.value.replace(/ /g, ""); // Удаление пробелов из введенного значения
|
||||
let inputNumbersOnly = inputVal.replace(/\D/g, ""); // Удаление всех символов, кроме цифр
|
||||
|
||||
if (inputNumbersOnly.length > 16) {
|
||||
inputNumbersOnly = inputNumbersOnly.substr(0, 16);
|
||||
if (inputNumbersOnly.length > 16) { // Если введенное значение превышает 16 символов
|
||||
inputNumbersOnly = inputNumbersOnly.substr(0, 16); // Усекаем его до 16 символов
|
||||
}
|
||||
|
||||
const splits = inputNumbersOnly.match(/.{1,4}/g);
|
||||
const splits = inputNumbersOnly.match(/.{1,4}/g); // Разделяем введенное значение на группы по 4 символа
|
||||
|
||||
let spacedNumber = "";
|
||||
if (splits) {
|
||||
spacedNumber = splits.join(" ");
|
||||
let spacedNumber = ""; // Строка для хранения введенного значения с разделителями
|
||||
if (splits) { // Если разделение прошло успешно
|
||||
spacedNumber = splits.join(" "); // Добавляем пробелы между группами по 4 символа
|
||||
}
|
||||
|
||||
setCcNumber(spacedNumber);
|
||||
setCcNumber(spacedNumber); // Устанавливаем введенное значение в состояние
|
||||
};
|
||||
|
||||
// Функция для обработки изменения даты истечения срока карты
|
||||
const handleChangeDate = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const inputValue = event.target.value;
|
||||
if (inputValue.length <= 4) {
|
||||
setValueDate(inputValue === '' ? '' : Number(inputValue));
|
||||
const handleChangeDate = (event: React.ChangeEvent<HTMLInputElement>) => { // Обработчик изменения даты истечения срока действия карты
|
||||
const inputValue = event.target.value; // Получаем введенное значение
|
||||
if (inputValue.length <= 4) { // Если введенное значение содержит не больше 4 символов
|
||||
setValueDate(inputValue === '' ? '' : Number(inputValue)); // Устанавливаем значение в состояние
|
||||
}
|
||||
};
|
||||
|
||||
// Функция для обработки изменения кода безопасности карты
|
||||
const handleChangeCode = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const inputValue = event.target.value;
|
||||
if (inputValue.length <= 3) {
|
||||
setValueCode(inputValue === '' ? '' : Number(inputValue));
|
||||
const handleChangeCode = (event: React.ChangeEvent<HTMLInputElement>) => { // Обработчик изменения кода карты
|
||||
const inputValue = event.target.value; // Получаем введенное значение
|
||||
if (inputValue.length <= 3) { // Если введенное значение содержит не больше 3 символов
|
||||
setValueCode(inputValue === '' ? '' : Number(inputValue)); // Устанавливаем значение в состояние
|
||||
}
|
||||
};
|
||||
|
||||
return(
|
||||
<section className="payment-page">
|
||||
{/* Отображение цены */}
|
||||
<h2 className="payment-page__price">{price} ₽ </h2>
|
||||
<div className="payment-page__payment-card">
|
||||
<h3 className="payment-card__heading"> Оплата картой </h3>
|
||||
{/* Ввод номера кредитной карты */}
|
||||
<input className="payment-card__input" type="text" placeholder="Номер" value={ccNumber} onChange={formatAndSetCcNumber}/>
|
||||
<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="CVC/CVV" value={valueCode} onChange={handleChangeCode}/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Кнопка для оплаты */}
|
||||
<a href="scam" className="payment-page__pay-link"> Оплатить </a>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Product, Reviews } from '../utils/types';
|
||||
import Review from '../components/Review';
|
||||
import axios from 'axios';
|
||||
@@ -8,150 +8,135 @@ import ReviewForm from '../components/ReviewForm';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
|
||||
export default function ProductPage() {
|
||||
function trimText(text: string, limit: number) { //сокращение описания
|
||||
return text.length > limit ? text.substring(0, limit) + '...' : text;
|
||||
}
|
||||
const { id } = useParams(); // Получение id из URL-параметров
|
||||
|
||||
const { id } = useParams(); //возвращает id товара из Url
|
||||
const [product, setProduct] = useState<Product | null>(null); //состояние для данных о товаре
|
||||
const [reviews, setReviews] = useState<Reviews[]>([]); //состаяние для отзывов
|
||||
const [averageRating, setAverageRating] = useState<number>(0); //состояние для средней арифметической оценки товара
|
||||
const [isDataFetched, setIsDataFetched] = useState(false); //состояние для отслеживания кэширования данных из запроса
|
||||
const totalReviews = reviews.length; //количество отзывов
|
||||
const countReviewsByRate = (rate: number): number => { //подсчёт отзывов с данной оценкой
|
||||
// Состояние для продукта и рецензий
|
||||
const [product, setProduct] = useState<Product | null>(null);
|
||||
const [reviews, setReviews] = useState<Reviews[]>([]);
|
||||
|
||||
// Состояние для среднего рейтинга и флага для отслеживания получения данных
|
||||
const [averageRating, setAverageRating] = useState<number>(0);
|
||||
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;
|
||||
};
|
||||
const percentageOfRate = (rate: number): number => { //расчёт процента отзывов с данной оценкой от количетсва всех отзывов
|
||||
|
||||
const percentageOfRate = (rate: number): number => { // Функция для вычисления процента рецензий по рейтингу
|
||||
const count = countReviewsByRate(rate);
|
||||
return (count / totalReviews) * 100;
|
||||
};
|
||||
|
||||
useEffect(() => { //запрос к api данных о товаре
|
||||
axios
|
||||
.get('http://127.0.0.1:8000/api/get/products')
|
||||
useEffect(() => { // Получение продукта по его id
|
||||
axios.get('http://127.0.0.1:8000/api/get/products')
|
||||
.then(response => {
|
||||
const productData = response.data.products.find( //"извлечение" данных о товаре из массива по его id
|
||||
const productData = response.data.products.find(
|
||||
(item: Product) => item.id.toString() === id
|
||||
);
|
||||
setProduct(productData);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('There was an error fetching the products', error);
|
||||
console.error('Ошибка при получении продукта:', error);
|
||||
});
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => { //запрос к api отзывов у товара
|
||||
if (!isDataFetched) { //проверка на кэширование данных
|
||||
axios
|
||||
.get(`http://127.0.0.1:8000/api/get/reviews/${id}`)
|
||||
useEffect(() => { // Получение рецензий по id продукта
|
||||
if (!isDataFetched) {
|
||||
axios.get(`http://127.0.0.1:8000/api/get/reviews/${id}`)
|
||||
.then(response => {
|
||||
setReviews(response.data.review);
|
||||
const totalRating = response.data.review.reduce((acc: number, review: Reviews) => acc + review.rate, 0); //общий рейтинг отзывов
|
||||
const average = totalRating / response.data.review.length; //средннее арифметический рейтинг всех отзывов
|
||||
if (response.data.review.length > 0) { //проверка на наличие отзывов
|
||||
setAverageRating(average);
|
||||
}
|
||||
else {
|
||||
setAverageRating(0);
|
||||
}
|
||||
const reviewsData = response.data.review;
|
||||
setReviews(reviewsData);
|
||||
const totalRating = reviewsData.reduce((acc: number, review: Reviews) => acc + review.rate, 0);
|
||||
const average = totalRating / reviewsData.length;
|
||||
setAverageRating(reviewsData.length > 0 ? average : 0);
|
||||
setIsDataFetched(true);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('There was an error fetching the reviews', error)
|
||||
})
|
||||
console.error('Ошибка при получении рецензий:', error);
|
||||
});
|
||||
}
|
||||
}, [id, isDataFetched])
|
||||
|
||||
if (!product) {
|
||||
return <div>Loading</div>;
|
||||
}, [id, isDataFetched]);
|
||||
|
||||
if (!product) { // Отображение загрузки, если продукт не загружен
|
||||
return <div>Загрузка...</div>;
|
||||
}
|
||||
|
||||
return(
|
||||
|
||||
return (
|
||||
<section className="product-page">
|
||||
<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">
|
||||
<span className="product-page__text-span">
|
||||
<h2 className="product-page__heading-h2">
|
||||
{product.title}
|
||||
</h2>
|
||||
<p className="product-page__short-desc-div">
|
||||
{trimText(product.description, 200)}
|
||||
</p>
|
||||
<h2 className="product-page__heading-h2">{product.title}</h2>
|
||||
<p className="product-page__short-desc-div">{trimText(product.description, 200)}</p>
|
||||
</span>
|
||||
<div className="product-page__container-div">
|
||||
<button className="product-page__share-button">
|
||||
<img src={ShareIcon as unknown as string} alt="" />
|
||||
<img src={ShareIcon} alt="Share" />
|
||||
</button>
|
||||
<div className="product-page__price-buy-div">
|
||||
<span className="product-page__price-span">
|
||||
{product.price} ₽
|
||||
</span>
|
||||
<Link to={`/payment?price=${product.price}`} className="product-page__buy-link">
|
||||
Купить
|
||||
</Link>
|
||||
<span className="product-page__price-span">{product.price} ₽</span>
|
||||
<Link to={`/payment?price=${product.price}`} className="product-page__buy-link">Купить</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section className="product-page__info-section">
|
||||
<h3 className="product-page__block-heading">
|
||||
Описание
|
||||
</h3>
|
||||
<p className="product-page__desc">
|
||||
{product.description}
|
||||
</p>
|
||||
<h3 className="product-page__block-heading">Описание</h3>
|
||||
<p className="product-page__desc">{product.description}</p>
|
||||
<ul className="product-page__tags-ul">
|
||||
{(product.tags.split('|')).map((tag, index) => ( //разделение тегов
|
||||
<li key={index} className="product-page__tag-li">
|
||||
{tag}
|
||||
</li>
|
||||
{product.tags.split('|').map((tag, index) => (
|
||||
<li key={index} className="product-page__tag-li">{tag}</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
<section className="product-page__reviews-section">
|
||||
<h3 className="product-page__block-heading">
|
||||
Отзывы
|
||||
</h3>
|
||||
<h3 className="product-page__block-heading">Отзывы</h3>
|
||||
<div className='reviews-section__rate-block'>
|
||||
<div className='rate-block__rating'>
|
||||
<span className='rate-block__rate-number'>
|
||||
{averageRating.toFixed(1)}
|
||||
</span>
|
||||
<span className='rate-block__rate-number'>{averageRating.toFixed(1)}</span>
|
||||
<div className="rate-block__star-rating">
|
||||
{/* Контейнер для отображения звезд, занимающий 100% ширины */}
|
||||
<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) => (
|
||||
<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 className='rate-block__progressbars-group'>
|
||||
{[5, 4, 3, 2, 1].map(rate => (
|
||||
<div className='progressbars-group__progressbar-container' key={rate}>
|
||||
<span className='rate-progressbar__rate-number'>
|
||||
{rate}
|
||||
</span>
|
||||
<div className='progressbar-container__progressbar'>
|
||||
<div className='progressbar__active-line' style={{ width: `${percentageOfRate(rate)}%` }}></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{[5, 4, 3, 2, 1].map(rate => (
|
||||
<div className='progressbars-group__progressbar-container' key={rate}>
|
||||
<span className='rate-progressbar__rate-number'>{rate}</span>
|
||||
<div className='progressbar-container__progressbar'>
|
||||
<div className='progressbar__active-line' style={{ width: `${percentageOfRate(rate)}%` }}></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'>
|
||||
{reviews.map((review) => (
|
||||
{reviews.map(review => (
|
||||
<Review key={review.id} review={review} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user