mirror of
https://github.com/yawaflua/SusMarket.git
synced 2025-12-09 20:19:31 +02:00
Commenting code and bringing it to the same style
This commit is contained in:
@@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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";
|
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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
}
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
@@ -30,5 +30,4 @@ export default function HomePage({ products }: HomePageProps) {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user