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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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