mirror of
https://github.com/yawaflua/SusMarket.git
synced 2025-12-08 19:49:36 +02:00
Code commenting & making code better
The same
This commit is contained in:
@@ -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
|
||||
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 Отзыва')),
|
||||
('commentary', models.TextField(max_length=300, 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='Дата создания отзыва')),
|
||||
('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')),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"files": {
|
||||
"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/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.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": [
|
||||
"static/css/main.f8545ce2.css",
|
||||
"static/js/main.142d0adb.js"
|
||||
"static/js/main.29450137.js"
|
||||
]
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>SusMarket</title><link rel="manifest" href="/manifest.json"/><script defer="defer" src="/static/js/main.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
3
reactapp/build/static/js/main.29450137.js
Normal file
3
reactapp/build/static/js/main.29450137.js
Normal file
File diff suppressed because one or more lines are too long
1
reactapp/build/static/js/main.29450137.js.map
Normal file
1
reactapp/build/static/js/main.29450137.js.map
Normal file
File diff suppressed because one or more lines are too long
@@ -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 PaymentPage from "./pages/PaymentPage";
|
||||
import ProductPage from "./pages/ProductPage";
|
||||
@@ -6,24 +9,26 @@ import ScamPage from "./pages/ScamPage";
|
||||
import InfoPage from "./pages/InfoPage";
|
||||
import Header from "./components/Header";
|
||||
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 axios from 'axios';
|
||||
|
||||
interface AppPopupMapState {
|
||||
isPopupMapVisible: boolean;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
// Состояние для отображения/скрытия карты
|
||||
const [state, setState] = useState<AppPopupMapState>({
|
||||
isPopupMapVisible: false,
|
||||
});
|
||||
|
||||
// Массив товаров
|
||||
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 => {
|
||||
@@ -34,37 +39,56 @@ export default function App() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Функция для переключения отображения/скрытия карты
|
||||
const togglePopupMap = () => {
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
isPopupMapVisible: !prevState.isPopupMapVisible,
|
||||
}));
|
||||
setState((prevState) => (
|
||||
{
|
||||
...prevState,
|
||||
isPopupMapVisible: !prevState.isPopupMapVisible,
|
||||
}
|
||||
));
|
||||
};
|
||||
|
||||
// Обработчик изменения поискового запроса
|
||||
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 handleSelectCategory = (category: Category | 'all') => {
|
||||
setSelectedCategory(category);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header togglePopupMap={togglePopupMap} onSelectCategory={handleSelectCategory} onSearchChange={handleSearchChange}/>
|
||||
{/* Шапка сайта */}
|
||||
<Header
|
||||
togglePopupMap={togglePopupMap}
|
||||
onSelectCategory={handleSelectCategory}
|
||||
onSearchChange={handleSearchChange}
|
||||
/>
|
||||
{/* Карта */}
|
||||
{state.isPopupMapVisible && <PopupMap togglePopupMap={togglePopupMap}/>}
|
||||
{/* Основной контент */}
|
||||
<main className="main">
|
||||
<Routes>
|
||||
{/* Главная страница */}
|
||||
<Route path="/" element={<HomePage products={filteredProducts}/>}/>
|
||||
{/* Страница профиля */}
|
||||
<Route path="profile/*" element={<ProfilePage />}/>
|
||||
{/* Страница продукта */}
|
||||
<Route path="product/:id" element={<ProductPage/>}/>
|
||||
{/* Страница оплаты */}
|
||||
<Route path="payment" element={<PaymentPage />}/>
|
||||
{/* Страница с описанием */}
|
||||
<Route path="scam" element={<ScamPage />}/>
|
||||
{/* Страница информации */}
|
||||
<Route path="info" element={<InfoPage />}/>
|
||||
</Routes>
|
||||
</main>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useEffect, useState} from "react";
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import axios from "axios";
|
||||
import { Category } from "../utils/types";
|
||||
|
||||
@@ -8,32 +8,40 @@ interface CatalogMenuProps {
|
||||
}
|
||||
|
||||
export default function CatalogMenu({ toggleCatalogMenu, onSelectCategory }: CatalogMenuProps): JSX.Element {
|
||||
const [categories, setCategories] = useState<Category[]>([]); //состояние для категорий
|
||||
|
||||
useEffect(() => { //запрос к api для получения категорий
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
|
||||
const fetchCategories = useCallback(async () => {
|
||||
try {
|
||||
const response = await axios.get('http://127.0.0.1:8000/api/get/category');
|
||||
setCategories(response.data.categories);
|
||||
} catch (error) {
|
||||
} catch (error) {
|
||||
console.error(`There was an error retrieving the data: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCategories();
|
||||
}, []);
|
||||
|
||||
return(
|
||||
}, [fetchCategories]);
|
||||
|
||||
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>
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,35 +14,22 @@ interface HeaderProps {
|
||||
onSearchChange: (query: string) => void;
|
||||
}
|
||||
|
||||
interface HeaderCatalogMenuState {
|
||||
isCatalogMenuVisible: boolean;
|
||||
}
|
||||
|
||||
interface HeaderLoginMenuState {
|
||||
isLoginMenuVisible: boolean;
|
||||
}
|
||||
|
||||
|
||||
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(!isCatalogMenuVisible);
|
||||
setIsCatalogMenuVisible(prevState => !prevState);
|
||||
};
|
||||
|
||||
const toggleLoginMenu = () => {
|
||||
setIsLoginMenuVisible(!isLoginMenuVisible);
|
||||
setIsLoginMenuVisible(prevState => !prevState);
|
||||
};
|
||||
|
||||
const handleProfileClick = () => {
|
||||
const userCookie = Cookies.get('user');
|
||||
if (userCookie) {
|
||||
navigate('/profile');
|
||||
} else {
|
||||
toggleLoginMenu();
|
||||
}
|
||||
userCookie ? navigate('/profile') : toggleLoginMenu();
|
||||
};
|
||||
|
||||
const resetCategoryFilter = () => {
|
||||
|
||||
@@ -23,11 +23,7 @@ export default function LoginMenu({ toggleLoginMenu }: LoginMenuProps): JSX.Elem
|
||||
let response;
|
||||
if (isRegistering) {
|
||||
// Регистрация пользователя
|
||||
const params = new URLSearchParams({
|
||||
login: login,
|
||||
password: password
|
||||
});
|
||||
response = await axios.get(`http://127.0.0.1:8000/api/post/user?${params.toString()}`);
|
||||
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)}`);
|
||||
|
||||
@@ -20,10 +20,16 @@ export default function PopupMap({ togglePopupMap }: PopupMapProps): JSX.Element
|
||||
<div className="popup-map__menu-div">
|
||||
<div className="menu-div__container-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 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>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@ import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import '../ProfileStyle.scss';
|
||||
import ProfileInfo from "./ProfileInfo";
|
||||
// import ProductCard from "./ProductCard";
|
||||
|
||||
export default function ProfilePurchases() {
|
||||
return(
|
||||
@@ -18,9 +17,7 @@ export default function ProfilePurchases() {
|
||||
<div className="purchases-container">
|
||||
<ProfileInfo />
|
||||
<div className="purchases-div">
|
||||
{/* <ProductCard ProductImg={ProductImage} ProductName="Абеме" Price={150}/>
|
||||
<ProductCard ProductImg={ProductImage} ProductName="Абеме" Price={1234523453}/>
|
||||
<ProductCard ProductImg={ProductImage} ProductName="Абеме" Price={10}/> */}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import { Reviews } from "../utils/types";
|
||||
import "../index.scss";
|
||||
import UserAvatar from "../assets/img/templateUser-avatar.png";
|
||||
@@ -7,57 +8,49 @@ type ReviewProps = {
|
||||
review: Reviews;
|
||||
};
|
||||
|
||||
export default function Review({ review }: ReviewProps) { //соответствие типов данных в review с указанными типами и свойствами в интерфейсе Reviews
|
||||
const readableDate = new Date(review.date).toLocaleDateString('ru-RU'); //приводит дату отзыва из запроса к api в читаемый вид (чч/мм/гг)
|
||||
|
||||
return(
|
||||
export default function Review({ review }: ReviewProps) {
|
||||
const [userName, setUserName] = useState<string>("");
|
||||
const readableDate = new Date(review.date).toLocaleDateString('ru-RU');
|
||||
|
||||
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">
|
||||
<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">
|
||||
{review.user_id}
|
||||
{userName}
|
||||
</h4>
|
||||
</div>
|
||||
<div className="review-container__review-info">
|
||||
<div className="review-info__star-rate">
|
||||
<input
|
||||
type="radio"
|
||||
className="star-rate__star-radio"
|
||||
value={1}
|
||||
aria-label="Плохо"
|
||||
checked={review.rate === 1}
|
||||
/>
|
||||
<input
|
||||
type="radio"
|
||||
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}
|
||||
/>
|
||||
{[1, 2, 3, 4, 5].map(rate => (
|
||||
<input
|
||||
key={rate}
|
||||
type="radio"
|
||||
className="star-rate__star-radio"
|
||||
value={rate}
|
||||
aria-label={rate === 1 ? "Плохо" : rate === 2 ? "Удовлетворительно" : rate === 3 ? "Нормально" : rate === 4 ? "Хорошо" : "Отлично"}
|
||||
checked={review.rate === rate}
|
||||
readOnly
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<time className="review-info__review-date" dateTime="2019-09-09">
|
||||
<time className="review-info__review-date" dateTime={new Date(review.date).toISOString()}>
|
||||
{readableDate}
|
||||
</time>
|
||||
</div>
|
||||
@@ -65,7 +58,7 @@ export default function Review({ review }: ReviewProps) { //соответств
|
||||
<p className="review-article__text-p">
|
||||
{review.commentary}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import '../ProductStyle.scss';
|
||||
import ImageAttachIcon from "../assets/icons/review-form__add-image-icon.svg";
|
||||
import { motion } from 'framer-motion';
|
||||
import axios from 'axios';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
interface ReviewState {
|
||||
text: string;
|
||||
rating: number;
|
||||
image: File | null;
|
||||
image?: string | ArrayBuffer | null; // Изображение может быть в формате base64
|
||||
}
|
||||
|
||||
export default function ReviewForm() {
|
||||
const [review, setReview] = useState<ReviewState>({ text: '', rating: 1, image: null });
|
||||
export default function ReviewForm({ productId }: { productId: string }) {
|
||||
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>) {
|
||||
setReview({ ...review, text: event.target.value });
|
||||
@@ -21,59 +31,85 @@ export default function ReviewForm() {
|
||||
}
|
||||
|
||||
function handleImageChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
if (event.target.files) {
|
||||
setReview({ ...review, image: event.target.files[0] });
|
||||
if (event.target.files && 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();
|
||||
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}>
|
||||
<h5 className='review-form__heading'>
|
||||
Оставить отзыв
|
||||
</h5>
|
||||
<div className="review-form__stars-container">
|
||||
{[...Array(5)].map((_, index) => (
|
||||
<input
|
||||
<input
|
||||
key={index}
|
||||
type="radio"
|
||||
className="review-form__star-radio"
|
||||
name="rating"
|
||||
value={index + 1}
|
||||
aria-label={`Рейтинг ${index + 1}`}
|
||||
checked={review.rating === index + 1}
|
||||
type="radio"
|
||||
className="review-form__star-radio"
|
||||
name="rating"
|
||||
value={index + 1}
|
||||
aria-label={`Рейтинг ${index + 1}`}
|
||||
checked={review.rating === index + 1}
|
||||
onChange={handleRatingChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<textarea
|
||||
className='review-form__textarea'
|
||||
cols={30}
|
||||
rows={5}
|
||||
placeholder='Комментарий'
|
||||
value={review.text}
|
||||
<textarea
|
||||
className='review-form__textarea'
|
||||
cols={30}
|
||||
rows={5}
|
||||
placeholder='Комментарий'
|
||||
value={review.text}
|
||||
onChange={handleTextChange}
|
||||
/>
|
||||
<label htmlFor="review-image" className='review-form__image-attach'>
|
||||
<img src={ImageAttachIcon} alt="Прикрепить изображение"/>
|
||||
<img src={ImageAttachIcon} alt="Прикрепить изображение" />
|
||||
</label>
|
||||
<input
|
||||
className='review-form__image-input'
|
||||
type="file"
|
||||
name="review image"
|
||||
id="review-image"
|
||||
accept='.png, .jpg, .jpeg'
|
||||
<input
|
||||
className='review-form__image-input'
|
||||
type="file"
|
||||
name="review image"
|
||||
id="review-image"
|
||||
accept='.png, .jpg, .jpeg'
|
||||
onChange={handleImageChange}
|
||||
/>
|
||||
<motion.button
|
||||
className='review-form__send-button'
|
||||
<motion.button
|
||||
className='review-form__send-button'
|
||||
type='submit'
|
||||
whileTap={{scale: 0.98}}
|
||||
transition={{duration: 0.2, type: "spring"}}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
transition={{ duration: 0.2, type: "spring" }}
|
||||
>
|
||||
Отправить отзыв
|
||||
</motion.button>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import '../HomeStyle.scss';
|
||||
import ProductCard from "../components/ProductCard";
|
||||
import Banner from "../components/AdBanner";
|
||||
import { Product } from "../utils/types";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
type HomePageProps = {
|
||||
products: Product[];
|
||||
}
|
||||
|
||||
export default function HomePage({ products }: HomePageProps) { //соответствие типов данных в products с указанными типами и свойствами в интерфейсе Product
|
||||
export default function HomePage({ products }: HomePageProps) {
|
||||
return(
|
||||
<section className="home-page">
|
||||
<Banner />
|
||||
@@ -18,16 +18,17 @@ export default function HomePage({ products }: HomePageProps) { //соответ
|
||||
<Link to={`/product/${product.id}`} key={product.id}>
|
||||
<ProductCard
|
||||
title={product.title}
|
||||
icons={product.icons}
|
||||
price={product.price}
|
||||
category_id={product.category_id}
|
||||
id={product.id}
|
||||
description={product.description}
|
||||
tags={product.tags}
|
||||
id={product.id}
|
||||
category_id={product.category_id}
|
||||
price={product.price}
|
||||
icons={product.icons}
|
||||
description={product.description}
|
||||
/>
|
||||
</Link>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import React from "react";
|
||||
import DevCard from "../components/DevCard";
|
||||
import "../InfoPageStyle.scss";
|
||||
import RailTHAvatar from "../assets/img/info-page__railth-avatar.png"
|
||||
import NoKesspenAvatar from "../assets/img/info-page__no-kesspen-avatar.png"
|
||||
import RailTHAvatar from "../assets/img/info-page__railth-avatar.png";
|
||||
import NoKesspenAvatar from "../assets/img/info-page__no-kesspen-avatar.png";
|
||||
|
||||
export default function InfoPage() {
|
||||
return(
|
||||
return (
|
||||
<section className="info-page">
|
||||
{/* Компонент DevCard отображает информацию о разработчике */}
|
||||
<DevCard
|
||||
avatar={NoKesspenAvatar}
|
||||
name="No_Kesspen"
|
||||
info="Backend разработчик"
|
||||
info="Backend & Frontend разработчик"
|
||||
url="https://github.com/KessPenGames"
|
||||
/>
|
||||
<DevCard
|
||||
@@ -20,5 +21,5 @@ export default function InfoPage() {
|
||||
url="https://github.com/Rail-TH"
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,37 +1,50 @@
|
||||
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 | ''>(''); //состояние кода безопасности карты
|
||||
|
||||
const formatAndSetCcNumber = (e: React.ChangeEvent<HTMLInputElement>) => { //в ccNumber максимум 16 цифр, разбивка пробелами по 4
|
||||
// Состояние номера кредитной карты
|
||||
const [ccNumber, setCcNumber] = useState("");
|
||||
// Состояние даты истечения срока карты
|
||||
const [valueDate, setValueDate] = useState<number | ''>('');
|
||||
// Состояние кода безопасности карты
|
||||
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, "");
|
||||
let inputNumbersOnly = inputVal.replace(/\D/g, "");
|
||||
|
||||
|
||||
if (inputNumbersOnly.length > 16) {
|
||||
inputNumbersOnly = inputNumbersOnly.substr(0, 16);
|
||||
}
|
||||
|
||||
const splits = inputNumbersOnly.match(/.{1,4}/g);
|
||||
|
||||
|
||||
let spacedNumber = "";
|
||||
if (splits) {
|
||||
spacedNumber = splits.join(" ");
|
||||
}
|
||||
|
||||
|
||||
setCcNumber(spacedNumber);
|
||||
};
|
||||
|
||||
const handleChangeDate = (event: React.ChangeEvent<HTMLInputElement>) => { //valueDate не более 4-х цифр
|
||||
// Функция для обработки изменения даты истечения срока карты
|
||||
const handleChangeDate = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const inputValue = event.target.value;
|
||||
if (inputValue.length <= 4) {
|
||||
setValueDate(inputValue === '' ? '' : Number(inputValue));
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangeCode = (event: React.ChangeEvent<HTMLInputElement>) => { //valueCode не более 3-х цифр
|
||||
// Функция для обработки изменения кода безопасности карты
|
||||
const handleChangeCode = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const inputValue = event.target.value;
|
||||
if (inputValue.length <= 3) {
|
||||
setValueCode(inputValue === '' ? '' : Number(inputValue));
|
||||
@@ -40,22 +53,21 @@ export default function PaymentPage() {
|
||||
|
||||
return(
|
||||
<section className="payment-page">
|
||||
<h2 className="payment-page__price">
|
||||
₽
|
||||
</h2>
|
||||
{/* Отображение цены */}
|
||||
<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" name="" id="" placeholder="Номер" value={ccNumber} onChange={formatAndSetCcNumber}/>
|
||||
<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" 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>
|
||||
<a href="scam" className="payment-page__pay-link">
|
||||
Оплатить
|
||||
</a>
|
||||
{/* Кнопка для оплаты */}
|
||||
<a href="scam" className="payment-page__pay-link"> Оплатить </a>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import axios from 'axios';
|
||||
import '../ProductStyle.scss';
|
||||
import ShareIcon from "../assets/icons/share-icon.svg";
|
||||
import ReviewForm from '../components/ReviewForm';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
|
||||
export default function ProductPage() {
|
||||
function trimText(text: string, limit: number) { //сокращение описания
|
||||
@@ -87,9 +87,9 @@ export default function ProductPage() {
|
||||
<span className="product-page__price-span">
|
||||
{product.price} ₽
|
||||
</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>
|
||||
@@ -144,7 +144,7 @@ export default function ProductPage() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<ReviewForm />
|
||||
<ReviewForm productId={product.id.toLocaleString('ru-RU')}/>
|
||||
<div className='product-page__reviews-container'>
|
||||
{reviews.map((review) => (
|
||||
<Review key={review.id} review={review} />
|
||||
@@ -153,4 +153,5 @@ export default function ProductPage() {
|
||||
</section>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,4 +15,4 @@ export default function ProfilePage() {
|
||||
</Routes>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user