Commenting code and bringing it to the same style

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -1,34 +1,36 @@
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> <div className="background-blackout" onClick={toggleCatalogMenu}></div>
{/* Список категорий */}
<ul className="catalog-menu"> <ul className="catalog-menu">
{categories.map((category) => ( {categories.map((category) => (
<li <li
key={category.id} key={category.id}
className="catalog-menu__point-li" className="catalog-menu__point-li"
onClick={() => onSelectCategory(category)} onClick={() => onSelectCategory(category)} // Выбор категории
> >
<img <img
className="catalog-menu__category-icon" className="catalog-menu__category-icon"

View File

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

View File

@@ -1,69 +1,65 @@
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(() => { useEffect(() => { // Эффект для добавления класса "no-scroll" с тела документа при монтировании компонента
document.body.classList.add('no-scroll'); document.body.classList.add('no-scroll');
return () => { return () => {
document.body.classList.remove('no-scroll'); document.body.classList.remove('no-scroll');
}; };
}, []); }, []);
const handleAuth = async (isRegistering: boolean) => { const handleAuth = async (isRegistering: boolean) => { // Функция для обработки авторизации
try { try {
let response; let response;
if (isRegistering) { if (isRegistering) {
// Регистрация пользователя response = await axios.get(
response = await axios.get(`http://127.0.0.1:8000/api/post/user?login=${encodeURIComponent(login)}&password=${encodeURIComponent(password)}`); `http://127.0.0.1:8000/api/post/user?login=${encodeURIComponent(login)}&password=${encodeURIComponent(password)}`
);
} else { } else {
// Вход в систему response = await axios.get(
response = await axios.get(`http://127.0.0.1:8000/api/get/user?login=${encodeURIComponent(login)}&password=${encodeURIComponent(password)}`); `http://127.0.0.1:8000/api/get/user?login=${encodeURIComponent(login)}&password=${encodeURIComponent(password)}`
// Проверка наличия пользователя );
if (response.data.user.length === 0) { if (response.data.user.length === 0) {
alert('Пользователь не найден.'); alert('Пользователь не найден.');
// Здесь можно выполнить действия в случае отсутствия пользователя, например, вывести сообщение об ошибке
return; return;
} }
} }
if (response.status === 200) { if (response.status === 200) {
// Создание cookie файла Cookies.set('user', login, { expires: 1 }); // Установка куки с логином
Cookies.set('user', login, { expires: 1 }); // Cookie на 1 день Cookies.set('user_id', response.data.user[0].id, { expires: 1 }); // Установка куки с ID пользователя
Cookies.set('user_id', response.data.user[0].id, { expires: 1 }) navigate('/profile'); // Переход на страницу профиля
// Перенаправление на страницу профиля toggleLoginMenu(); // Вызов функции переключения видимости меню входа
navigate('/profile');
toggleLoginMenu(); // Закрытие меню входа
} }
} catch (error) { } catch (error) {
alert('Ошибка при авторизации: ' + error); alert('Ошибка при авторизации: ' + error);
} }
} };
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { // Функция для обработки отправки формы
event.preventDefault(); event.preventDefault();
await handleAuth(!isLoginMode); await handleAuth(!isLoginMode);
} };
return ( return (
<> <>
@@ -80,8 +76,24 @@ export default function LoginMenu({ toggleLoginMenu }: LoginMenuProps): JSX.Elem
</div> </div>
</div> </div>
<div className="popup-login__inputs-container"> <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
<input type="password" name="userPassword" id="userPassword" className="popup-login__password-input" placeholder="Пароль" value={password} onChange={(e) => setPassword(e.target.value)}/> 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>
<div className="popup-login__bottom-container"> <div className="popup-login__bottom-container">
<p className="popup-login__prompt-url" onClick={toggleMode}> <p className="popup-login__prompt-url" onClick={toggleMode}>
@@ -92,12 +104,12 @@ export default function LoginMenu({ toggleLoginMenu }: LoginMenuProps): JSX.Elem
type="submit" type="submit"
className="popup-login__login-button" className="popup-login__login-button"
whileTap={{ scale: 0.98 }} whileTap={{ scale: 0.98 }}
transition={{duration: 0.2, type: "spring"}} transition={{ duration: 0.2, type: 'spring' }}
> >
{isLoginMode ? 'Войти' : 'Зарегистрироваться'} {isLoginMode ? 'Войти' : 'Зарегистрироваться'}
</motion.button> </motion.button>
</div> </div>
</form> </form>
</> </>
) );
} }

View File

@@ -1,25 +1,26 @@
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 = () => { const handleClose = () => { // Обработчик закрытия всплывающего окна
document.body.classList.remove('no-scroll'); document.body.classList.remove('no-scroll'); // Удаление класса "no-scroll" с тела документа
togglePopupMap(); togglePopupMap(); // Вызов функции для закрытия всплывающего окна
}; };
useEffect(() => { useEffect(() => { // Эффект для добавления класса "no-scroll" с тела документа при монтировании компонента
document.body.classList.add('no-scroll'); document.body.classList.add('no-scroll');
return () => { return () => {
document.body.classList.remove('no-scroll'); document.body.classList.remove('no-scroll');
@@ -37,7 +38,7 @@ export default function PopupMap({ togglePopupMap }: PopupMapProps) {
className={`delivery-div__delivery-button ${selectedButton === 1 ? 'delivery-div__delivery-button_selected' : ''}`} className={`delivery-div__delivery-button ${selectedButton === 1 ? 'delivery-div__delivery-button_selected' : ''}`}
onClick={() => handleButtonClick(1)} onClick={() => handleButtonClick(1)}
whileTap={{ scale: 0.98 }} whileTap={{ scale: 0.98 }}
transition={{duration: 0.02, type: "spring"}} transition={{ duration: 0.02, type: 'spring' }}
> >
Самовывоз Самовывоз
</motion.button> </motion.button>
@@ -45,7 +46,7 @@ export default function PopupMap({ togglePopupMap }: PopupMapProps) {
className={`delivery-div__delivery-button ${selectedButton === 2 ? 'delivery-div__delivery-button_selected' : ''}`} className={`delivery-div__delivery-button ${selectedButton === 2 ? 'delivery-div__delivery-button_selected' : ''}`}
onClick={() => handleButtonClick(2)} onClick={() => handleButtonClick(2)}
whileTap={{ scale: 0.98 }} whileTap={{ scale: 0.98 }}
transition={{duration: 0.02, type: "spring"}} transition={{ duration: 0.02, type: 'spring' }}
> >
Курьером Курьером
</motion.button> </motion.button>
@@ -55,7 +56,7 @@ export default function PopupMap({ togglePopupMap }: PopupMapProps) {
<motion.button <motion.button
className="menu-div__select-button" className="menu-div__select-button"
whileTap={{ scale: 0.98 }} whileTap={{ scale: 0.98 }}
transition={{duration: 0.01, type: "spring"}} transition={{ duration: 0.01, type: 'spring' }}
> >
Заберу здесь Заберу здесь
</motion.button> </motion.button>
@@ -63,9 +64,16 @@ export default function PopupMap({ togglePopupMap }: PopupMapProps) {
<div className="popup-map__map-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/?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> <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> <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> </div>
</> </>
) );
} }

View File

@@ -8,20 +8,14 @@ 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 => {
@@ -29,21 +23,15 @@ export default function Review({ review }: ReviewProps) {
}); });
}, [review.user_id]); }, [review.user_id]);
// Возвращаем JSX для отображения отзыва
return ( return (
<article className="review-article"> <article className="review-article">
<div className="review-article__review-container"> <div className="review-article__review-container">
<div className="review-container__user-info"> <div className="review-container__user-info">
{/* Отображаем аватарку пользователя */}
<img className="user-info__user-avatar" src={UserAvatar} alt="Review user avatar" /> <img className="user-info__user-avatar" src={UserAvatar} alt="Review user avatar" />
<h4 className="user-info__user-name"> <h4 className="user-info__user-name">{userName}</h4>
{/* Отображаем логин пользователя */}
{userName}
</h4>
</div> </div>
<div className="review-container__review-info"> <div className="review-container__review-info">
<div className="review-info__star-rate"> <div className="review-info__star-rate">
{/* Отображаем рейтинг отзыва */}
{[1, 2, 3, 4, 5].map(rate => ( {[1, 2, 3, 4, 5].map(rate => (
<input <input
key={rate} key={rate}
@@ -57,18 +45,12 @@ export default function Review({ review }: ReviewProps) {
))} ))}
</div> </div>
<time className="review-info__review-date" dateTime={new Date(review.date).toISOString()}> <time className="review-info__review-date" dateTime={new Date(review.date).toISOString()}>
{/* Отображаем дату отзыва */}
{readableDate} {readableDate}
</time> </time>
</div> </div>
</div> </div>
<p className="review-article__text-p"> <p className="review-article__text-p">{review.commentary}</p>
{/* Отображаем текст отзыва */}
{review.commentary}
</p>
{/* Отображаем изображение товара, если оно есть */}
{review.icons && <img className="review-article__product-image" src={review.icons} alt="Review product" />} {review.icons && <img className="review-article__product-image" src={review.icons} alt="Review product" />}
</article> </article>
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,122 +8,110 @@ 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) => ( //пожалуйста, не спрашивайте как это работает {/* Отображение звезд, которые не должны быть закрашены */}
{'★★★★★'.split('').map((star, i) => (
<span key={`back-star-${i}`}>{star}</span> <span key={`back-star-${i}`}>{star}</span>
))} ))}
<div className="star-rating__front-stars" style={{ width: `${(averageRating / 5) * 100}%` }}> {/* Контейнер для отображения звезд, которые должны быть закрашены */}
<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={`front-star-${i}`}>{star}</span>
))} ))}
@@ -134,9 +122,7 @@ export default function ProductPage() {
<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}
</span>
<div className='progressbar-container__progressbar'> <div className='progressbar-container__progressbar'>
<div className='progressbar__active-line' style={{ width: `${percentageOfRate(rate)}%` }}></div> <div className='progressbar__active-line' style={{ width: `${percentageOfRate(rate)}%` }}></div>
</div> </div>
@@ -146,12 +132,11 @@ export default function ProductPage() {
</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>
) );
} }