Code commenting & making code better

The same
This commit is contained in:
RailTH
2024-05-17 14:52:16 +11:00
parent 03974cb28f
commit 1c4aef92e8
21 changed files with 248 additions and 186 deletions

View File

@@ -1,4 +1,4 @@
# Generated by Django 4.1 on 2024-05-14 07:21 # Generated by Django 4.1 on 2024-05-16 07:15
import django.core.validators import django.core.validators
from django.db import migrations, models from django.db import migrations, models
@@ -61,7 +61,7 @@ class Migration(migrations.Migration):
('id', models.AutoField(db_index=True, primary_key=True, serialize=False, unique=True, verbose_name='ID Отзыва')), ('id', models.AutoField(db_index=True, primary_key=True, serialize=False, unique=True, verbose_name='ID Отзыва')),
('commentary', models.TextField(max_length=300, verbose_name='Комментарий отзыва')), ('commentary', models.TextField(max_length=300, verbose_name='Комментарий отзыва')),
('rate', models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)], verbose_name='Оценка')), ('rate', models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)], verbose_name='Оценка')),
('icons', models.TextField(null=True, verbose_name='Изображение отзыва в BASE64')), ('icons', models.TextField(blank=True, null=True, verbose_name='Изображение отзыва в BASE64')),
('date', models.DateTimeField(auto_now=True, verbose_name='Дата создания отзыва')), ('date', models.DateTimeField(auto_now=True, verbose_name='Дата создания отзыва')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='SusMarketBackend.product')), ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='SusMarketBackend.product')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='SusMarketBackend.user')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='SusMarketBackend.user')),

View File

@@ -1,7 +1,7 @@
{ {
"files": { "files": {
"main.css": "/static/css/main.f8545ce2.css", "main.css": "/static/css/main.f8545ce2.css",
"main.js": "/static/js/main.142d0adb.js", "main.js": "/static/js/main.29450137.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.f8545ce2.css.map": "/static/css/main.f8545ce2.css.map", "main.f8545ce2.css.map": "/static/css/main.f8545ce2.css.map",
"main.142d0adb.js.map": "/static/js/main.142d0adb.js.map" "main.29450137.js.map": "/static/js/main.29450137.js.map"
}, },
"entrypoints": [ "entrypoints": [
"static/css/main.f8545ce2.css", "static/css/main.f8545ce2.css",
"static/js/main.142d0adb.js" "static/js/main.29450137.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.142d0adb.js"></script><link href="/static/css/main.f8545ce2.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.29450137.js"></script><link href="/static/css/main.f8545ce2.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

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,6 @@
import React, { useState, useEffect } from "react";
import { Routes, Route } from 'react-router-dom';
import axios from 'axios';
import HomePage from "./pages/HomePage"; import HomePage from "./pages/HomePage";
import PaymentPage from "./pages/PaymentPage"; import PaymentPage from "./pages/PaymentPage";
import ProductPage from "./pages/ProductPage"; import ProductPage from "./pages/ProductPage";
@@ -6,24 +9,26 @@ import ScamPage from "./pages/ScamPage";
import InfoPage from "./pages/InfoPage"; import InfoPage from "./pages/InfoPage";
import Header from "./components/Header"; import Header from "./components/Header";
import PopupMap from "./components/PopupMap"; import PopupMap from "./components/PopupMap";
import React, { useState, useEffect } from "react";
import { Routes, Route } from 'react-router-dom';
import { Product, Category } from "./utils/types"; import { Product, Category } from "./utils/types";
import axios from 'axios';
interface AppPopupMapState { interface AppPopupMapState {
isPopupMapVisible: boolean; isPopupMapVisible: boolean;
} }
export default function App() { export default function App() {
// Состояние для отображения/скрытия карты
const [state, setState] = useState<AppPopupMapState>({ const [state, setState] = useState<AppPopupMapState>({
isPopupMapVisible: false, 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 => {
@@ -34,37 +39,56 @@ export default function App() {
}); });
}, []); }, []);
// Функция для переключения отображения/скрытия карты
const togglePopupMap = () => { const togglePopupMap = () => {
setState((prevState) => ({ setState((prevState) => (
...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 togglePopupMap={togglePopupMap} onSelectCategory={handleSelectCategory} onSearchChange={handleSearchChange}/> {/* Шапка сайта */}
<Header
togglePopupMap={togglePopupMap}
onSelectCategory={handleSelectCategory}
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,4 +1,4 @@
import React, {useEffect, useState} from "react"; import React, { useEffect, useState, useCallback } from "react";
import axios from "axios"; import axios from "axios";
import { Category } from "../utils/types"; import { Category } from "../utils/types";
@@ -8,32 +8,40 @@ interface CatalogMenuProps {
} }
export default function CatalogMenu({ toggleCatalogMenu, onSelectCategory }: CatalogMenuProps): JSX.Element { export default function CatalogMenu({ toggleCatalogMenu, onSelectCategory }: CatalogMenuProps): JSX.Element {
const [categories, setCategories] = useState<Category[]>([]); //состояние для категорий const [categories, setCategories] = useState<Category[]>([]);
useEffect(() => { //запрос к api для получения категорий const fetchCategories = useCallback(async () => {
const fetchCategories = async () => { try {
try {
const response = await axios.get('http://127.0.0.1:8000/api/get/category'); const response = await axios.get('http://127.0.0.1:8000/api/get/category');
setCategories(response.data.categories); setCategories(response.data.categories);
} catch (error) { } catch (error) {
console.error(`There was an error retrieving the data: ${error}`); console.error(`There was an error retrieving the data: ${error}`);
} }
}; }, []);
useEffect(() => {
fetchCategories(); fetchCategories();
}, []); }, [fetchCategories]);
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 key={category.id} className="catalog-menu__point-li" onClick={() => onSelectCategory(category)}> <li
<img className="catalog-menu__category-icon" src={category.image} alt={category.title} /> key={category.id}
{category.title} className="catalog-menu__point-li"
</li> onClick={() => onSelectCategory(category)}
>
<img
className="catalog-menu__category-icon"
src={category.image}
alt={category.title}
/>
{category.title}
</li>
))} ))}
</ul> </ul>
</> </>
) );
} }

View File

@@ -14,35 +14,22 @@ interface HeaderProps {
onSearchChange: (query: string) => void; onSearchChange: (query: string) => void;
} }
interface HeaderCatalogMenuState {
isCatalogMenuVisible: boolean;
}
interface HeaderLoginMenuState {
isLoginMenuVisible: boolean;
}
export default function Header({ togglePopupMap, onSelectCategory, onSearchChange }: HeaderProps): JSX.Element { export default function Header({ togglePopupMap, onSelectCategory, onSearchChange }: HeaderProps): JSX.Element {
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(!isCatalogMenuVisible); setIsCatalogMenuVisible(prevState => !prevState);
}; };
const toggleLoginMenu = () => { const toggleLoginMenu = () => {
setIsLoginMenuVisible(!isLoginMenuVisible); setIsLoginMenuVisible(prevState => !prevState);
}; };
const handleProfileClick = () => { const handleProfileClick = () => {
const userCookie = Cookies.get('user'); const userCookie = Cookies.get('user');
if (userCookie) { userCookie ? navigate('/profile') : toggleLoginMenu();
navigate('/profile');
} else {
toggleLoginMenu();
}
}; };
const resetCategoryFilter = () => { const resetCategoryFilter = () => {

View File

@@ -23,11 +23,7 @@ export default function LoginMenu({ toggleLoginMenu }: LoginMenuProps): JSX.Elem
let response; let response;
if (isRegistering) { if (isRegistering) {
// Регистрация пользователя // Регистрация пользователя
const params = new URLSearchParams({ response = await axios.get(`http://127.0.0.1:8000/api/post/user?login=${encodeURIComponent(login)}&password=${encodeURIComponent(password)}`);
login: login,
password: password
});
response = await axios.get(`http://127.0.0.1:8000/api/post/user?${params.toString()}`);
} else { } else {
// Вход в систему // Вход в систему
response = await axios.get(`http://127.0.0.1:8000/api/get/user?login=${encodeURIComponent(login)}&password=${encodeURIComponent(password)}`); response = await axios.get(`http://127.0.0.1:8000/api/get/user?login=${encodeURIComponent(login)}&password=${encodeURIComponent(password)}`);

View File

@@ -20,10 +20,16 @@ export default function PopupMap({ togglePopupMap }: PopupMapProps): JSX.Element
<div className="popup-map__menu-div"> <div className="popup-map__menu-div">
<div className="menu-div__container-div"> <div className="menu-div__container-div">
<div className="menu-div__delivery-div"> <div className="menu-div__delivery-div">
<button className={selectedButton === 1 ? 'delivery-div__delivery-button delivery-div__delivery-button_selected' : 'delivery-div__delivery-button'} onClick={() => handleButtonClick(1)}> <button
className={`delivery-div__delivery-button ${selectedButton === 1 ? 'delivery-div__delivery-button_selected' : ''}`}
onClick={() => handleButtonClick(1)}
>
Самовывоз Самовывоз
</button> </button>
<button className={selectedButton === 2 ? 'delivery-div__delivery-button delivery-div__delivery-button_selected' : 'delivery-div__delivery-button'} onClick={() => handleButtonClick(2)}> <button
className={`delivery-div__delivery-button ${selectedButton === 2 ? 'delivery-div__delivery-button_selected' : ''}`}
onClick={() => handleButtonClick(2)}
>
Курьером Курьером
</button> </button>
</div> </div>

View File

@@ -2,7 +2,6 @@ import React from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import '../ProfileStyle.scss'; import '../ProfileStyle.scss';
import ProfileInfo from "./ProfileInfo"; import ProfileInfo from "./ProfileInfo";
// import ProductCard from "./ProductCard";
export default function ProfilePurchases() { export default function ProfilePurchases() {
return( return(
@@ -18,9 +17,7 @@ export default function ProfilePurchases() {
<div className="purchases-container"> <div className="purchases-container">
<ProfileInfo /> <ProfileInfo />
<div className="purchases-div"> <div className="purchases-div">
{/* <ProductCard ProductImg={ProductImage} ProductName="Абеме" Price={150}/>
<ProductCard ProductImg={ProductImage} ProductName="Абеме" Price={1234523453}/>
<ProductCard ProductImg={ProductImage} ProductName="Абеме" Price={10}/> */}
</div> </div>
</div> </div>
</section> </section>

View File

@@ -1,4 +1,5 @@
import React from "react"; import React, { useState, useEffect } from "react";
import axios from "axios";
import { Reviews } from "../utils/types"; import { Reviews } from "../utils/types";
import "../index.scss"; import "../index.scss";
import UserAvatar from "../assets/img/templateUser-avatar.png"; import UserAvatar from "../assets/img/templateUser-avatar.png";
@@ -7,57 +8,49 @@ type ReviewProps = {
review: Reviews; review: Reviews;
}; };
export default function Review({ review }: ReviewProps) { //соответствие типов данных в review с указанными типами и свойствами в интерфейсе Reviews export default function Review({ review }: ReviewProps) {
const readableDate = new Date(review.date).toLocaleDateString('ru-RU'); //приводит дату отзыва из запроса к api в читаемый вид (чч/мм/гг) const [userName, setUserName] = useState<string>("");
const readableDate = new Date(review.date).toLocaleDateString('ru-RU');
return(
useEffect(() => {
const fetchUserName = async () => {
try {
const response = await axios.get(`http://127.0.0.1:8000/api/get/user/${review.user_id}`);
if (response.status === 200) {
setUserName(response.data.login);
}
} catch (error) {
console.error('Ошибка при получении логина пользователя:', error);
}
};
fetchUserName();
}, [review.user_id]);
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">
{review.user_id} {userName}
</h4> </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">
<input {[1, 2, 3, 4, 5].map(rate => (
type="radio" <input
className="star-rate__star-radio" key={rate}
value={1} type="radio"
aria-label="Плохо" className="star-rate__star-radio"
checked={review.rate === 1} value={rate}
/> aria-label={rate === 1 ? "Плохо" : rate === 2 ? "Удовлетворительно" : rate === 3 ? "Нормально" : rate === 4 ? "Хорошо" : "Отлично"}
<input checked={review.rate === rate}
type="radio" readOnly
className="star-rate__star-radio" />
value={2} ))}
aria-label="Удовлетворительно"
checked={review.rate === 2}
/>
<input
type="radio"
className="star-rate__star-radio"
value={3}
aria-label="Нормально"
checked={review.rate === 3}
/>
<input
type="radio"
className="star-rate__star-radio"
value={4}
aria-label="Хорошо"
checked={review.rate === 4}
/>
<input
type="radio"
className="star-rate__star-radio"
value={5}
aria-label="Отлично"
checked={review.rate === 5}
/>
</div> </div>
<time className="review-info__review-date" dateTime="2019-09-09"> <time className="review-info__review-date" dateTime={new Date(review.date).toISOString()}>
{readableDate} {readableDate}
</time> </time>
</div> </div>
@@ -65,7 +58,7 @@ export default function Review({ review }: ReviewProps) { //соответств
<p className="review-article__text-p"> <p className="review-article__text-p">
{review.commentary} {review.commentary}
</p> </p>
<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

@@ -1,16 +1,26 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import '../ProductStyle.scss'; import '../ProductStyle.scss';
import ImageAttachIcon from "../assets/icons/review-form__add-image-icon.svg"; import ImageAttachIcon from "../assets/icons/review-form__add-image-icon.svg";
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import axios from 'axios';
import Cookies from 'js-cookie';
interface ReviewState { interface ReviewState {
text: string; text: string;
rating: number; rating: number;
image: File | null; image?: string | ArrayBuffer | null; // Изображение может быть в формате base64
} }
export default function ReviewForm() { export default function ReviewForm({ productId }: { productId: string }) {
const [review, setReview] = useState<ReviewState>({ text: '', rating: 1, image: null }); const [review, setReview] = useState<ReviewState>({ text: '', rating: 1 });
const [userId, setUserId] = useState<string | null>(null);
useEffect(() => {
const userIdFromCookie = Cookies.get('id');
if (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 });
@@ -21,59 +31,85 @@ export default function ReviewForm() {
} }
function handleImageChange(event: React.ChangeEvent<HTMLInputElement>) { function handleImageChange(event: React.ChangeEvent<HTMLInputElement>) {
if (event.target.files) { if (event.target.files && event.target.files[0]) {
setReview({ ...review, image: event.target.files[0] }); const reader = new FileReader();
reader.onload = () => {
setReview({ ...review, image: reader.result });
};
reader.readAsDataURL(event.target.files[0]);
} }
} }
function handleSubmit(event: React.FormEvent) { async function handleSubmit(event: React.FormEvent) {
event.preventDefault(); event.preventDefault();
console.log(review); if (!userId) {
console.error('User ID not found!');
return;
}
try {
const formData = new FormData();
formData.append('product_id', productId);
formData.append('text', review.text);
formData.append('rating', String(review.rating));
if (review.image) {
formData.append('image', review.image as string);
}
formData.append('user_id', userId);
await axios.post('http://127.0.0.1:8000/api/post/review', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
console.log('Review submitted successfully!');
// Опционально: добавить логику для обновления списка отзывов на странице после успешной отправки
} catch (error) {
console.error('Error submitting review:', 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}
type="radio" type="radio"
className="review-form__star-radio" className="review-form__star-radio"
name="rating" name="rating"
value={index + 1} value={index + 1}
aria-label={`Рейтинг ${index + 1}`} aria-label={`Рейтинг ${index + 1}`}
checked={review.rating === index + 1} checked={review.rating === index + 1}
onChange={handleRatingChange} onChange={handleRatingChange}
/> />
))} ))}
</div> </div>
<textarea <textarea
className='review-form__textarea' className='review-form__textarea'
cols={30} cols={30}
rows={5} rows={5}
placeholder='Комментарий' placeholder='Комментарий'
value={review.text} value={review.text}
onChange={handleTextChange} onChange={handleTextChange}
/> />
<label htmlFor="review-image" className='review-form__image-attach'> <label htmlFor="review-image" className='review-form__image-attach'>
<img src={ImageAttachIcon} alt="Прикрепить изображение"/> <img src={ImageAttachIcon} alt="Прикрепить изображение" />
</label> </label>
<input <input
className='review-form__image-input' className='review-form__image-input'
type="file" type="file"
name="review image" name="review image"
id="review-image" id="review-image"
accept='.png, .jpg, .jpeg' accept='.png, .jpg, .jpeg'
onChange={handleImageChange} onChange={handleImageChange}
/> />
<motion.button <motion.button
className='review-form__send-button' className='review-form__send-button'
type='submit' type='submit'
whileTap={{scale: 0.98}} whileTap={{ scale: 0.98 }}
transition={{duration: 0.2, type: "spring"}} transition={{ duration: 0.2, type: "spring" }}
> >
Отправить отзыв Отправить отзыв
</motion.button> </motion.button>

View File

@@ -1,15 +1,15 @@
import React from "react"; import React from "react";
import { Link } from "react-router-dom";
import '../HomeStyle.scss'; import '../HomeStyle.scss';
import ProductCard from "../components/ProductCard"; import ProductCard from "../components/ProductCard";
import Banner from "../components/AdBanner"; import Banner from "../components/AdBanner";
import { Product } from "../utils/types"; import { Product } from "../utils/types";
import { Link } from "react-router-dom";
type HomePageProps = { type HomePageProps = {
products: Product[]; products: Product[];
} }
export default function HomePage({ products }: HomePageProps) { //соответствие типов данных в products с указанными типами и свойствами в интерфейсе Product export default function HomePage({ products }: HomePageProps) {
return( return(
<section className="home-page"> <section className="home-page">
<Banner /> <Banner />
@@ -18,16 +18,17 @@ export default function HomePage({ products }: HomePageProps) { //соответ
<Link to={`/product/${product.id}`} key={product.id}> <Link to={`/product/${product.id}`} key={product.id}>
<ProductCard <ProductCard
title={product.title} title={product.title}
icons={product.icons}
price={product.price}
category_id={product.category_id}
id={product.id}
description={product.description}
tags={product.tags} tags={product.tags}
id={product.id}
category_id={product.category_id}
price={product.price}
icons={product.icons}
description={product.description}
/> />
</Link> </Link>
))} ))}
</div> </div>
</section> </section>
); );
} }

View File

@@ -1,16 +1,17 @@
import React from "react"; import React from "react";
import DevCard from "../components/DevCard"; import DevCard from "../components/DevCard";
import "../InfoPageStyle.scss"; import "../InfoPageStyle.scss";
import RailTHAvatar from "../assets/img/info-page__railth-avatar.png" import RailTHAvatar from "../assets/img/info-page__railth-avatar.png";
import NoKesspenAvatar from "../assets/img/info-page__no-kesspen-avatar.png" 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"
info="Backend разработчик" info="Backend & Frontend разработчик"
url="https://github.com/KessPenGames" url="https://github.com/KessPenGames"
/> />
<DevCard <DevCard
@@ -20,5 +21,5 @@ export default function InfoPage() {
url="https://github.com/Rail-TH" url="https://github.com/Rail-TH"
/> />
</section> </section>
) );
} }

View File

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

View File

@@ -5,7 +5,7 @@ import axios from 'axios';
import '../ProductStyle.scss'; import '../ProductStyle.scss';
import ShareIcon from "../assets/icons/share-icon.svg"; import ShareIcon from "../assets/icons/share-icon.svg";
import ReviewForm from '../components/ReviewForm'; import ReviewForm from '../components/ReviewForm';
import { 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) { //сокращение описания function trimText(text: string, limit: number) { //сокращение описания
@@ -87,9 +87,9 @@ export default function ProductPage() {
<span className="product-page__price-span"> <span className="product-page__price-span">
{product.price} {product.price}
</span> </span>
<a href="/payment" className="product-page__buy-link"> <Link to={`/payment?price=${product.price}`} className="product-page__buy-link">
Купить Купить
</a> </Link>
</div> </div>
</div> </div>
</div> </div>
@@ -144,7 +144,7 @@ export default function ProductPage() {
))} ))}
</div> </div>
</div> </div>
<ReviewForm /> <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} />
@@ -153,4 +153,5 @@ export default function ProductPage() {
</section> </section>
</section> </section>
) )
} }

View File

@@ -15,4 +15,4 @@ export default function ProfilePage() {
</Routes> </Routes>
</section> </section>
) )
} }