diff --git a/backend/__pycache__/main.cpython-310.pyc b/backend/__pycache__/main.cpython-310.pyc index 8d072aa..0520daa 100644 Binary files a/backend/__pycache__/main.cpython-310.pyc and b/backend/__pycache__/main.cpython-310.pyc differ diff --git a/backend/api/v0/__pycache__/feedback.cpython-310.pyc b/backend/api/v0/__pycache__/feedback.cpython-310.pyc index 9031a0f..ec3d5d2 100644 Binary files a/backend/api/v0/__pycache__/feedback.cpython-310.pyc and b/backend/api/v0/__pycache__/feedback.cpython-310.pyc differ diff --git a/backend/api/v0/__pycache__/payments.cpython-310.pyc b/backend/api/v0/__pycache__/payments.cpython-310.pyc new file mode 100644 index 0000000..848d9b5 Binary files /dev/null and b/backend/api/v0/__pycache__/payments.cpython-310.pyc differ diff --git a/backend/api/v0/__pycache__/poster.cpython-310.pyc b/backend/api/v0/__pycache__/poster.cpython-310.pyc index 28dfac6..72fada7 100644 Binary files a/backend/api/v0/__pycache__/poster.cpython-310.pyc and b/backend/api/v0/__pycache__/poster.cpython-310.pyc differ diff --git a/backend/api/v0/__pycache__/user.cpython-310.pyc b/backend/api/v0/__pycache__/user.cpython-310.pyc index 67667ab..80cc9fb 100644 Binary files a/backend/api/v0/__pycache__/user.cpython-310.pyc and b/backend/api/v0/__pycache__/user.cpython-310.pyc differ diff --git a/backend/api/v0/payments.py b/backend/api/v0/payments.py new file mode 100644 index 0000000..44339b1 --- /dev/null +++ b/backend/api/v0/payments.py @@ -0,0 +1,212 @@ +from fastapi import APIRouter, Depends, HTTPException, Body, Header, Request +from sqlalchemy.orm import Session +from core.db import get_db, Sale, User, Poster +from core.pay import YooKassaPayment +from pydantic import BaseModel, Field +from typing import Optional, List +from datetime import datetime +import json + +class PaymentRequest(BaseModel): + amount: float + currency: str + description: str + return_url: str + user_id: Optional[int] = Field(None) # Делаем необязательным + poster_id: Optional[int] = None + +router = APIRouter() + +# Конфигурация YooKassa +SHOP_ID = "1017909" +API_KEY = "test_udCBy7nXqGavVoj3RrzIRXN9UL02_UnF0FBBykxWH60" +payment_service = YooKassaPayment(SHOP_ID, API_KEY) + +@router.post("/create-payment/") +async def create_payment( + payment_data: dict = Body(...), # Принимаем любой JSON + db: Session = Depends(get_db) +): + try: + # Логирование для отладки + print(f"Received payment data: {json.dumps(payment_data, ensure_ascii=False)}") + + # Создание платежа через YooKassa + payment_response = payment_service.create_payment( + amount=float(payment_data.get("amount", 0)), + currency=payment_data.get("currency", "RUB"), + description=payment_data.get("description", ""), + return_url=payment_data.get("return_url", "http://localhost:3000/payments") + ) + + # Логирование ответа YooKassa + print(f"YooKassa response: {json.dumps(payment_response, ensure_ascii=False)}") + + # Сохранение данных о платеже в БД + new_sale = Sale( + amount=float(payment_data.get("amount", 0)), + status="pending", + description=payment_data.get("description", ""), + poster_id=payment_data.get("poster_id"), + user_id=payment_data.get("user_id", 1), # Используем 1 как значение по умолчанию + user_email=payment_data.get("user_email", "example@example.com"), + payment_id=payment_response["id"], + confirmation_url=payment_response["confirmation"]["confirmation_url"] + ) + db.add(new_sale) + db.commit() + db.refresh(new_sale) + + return {"confirmation_url": new_sale.confirmation_url, "payment_id": payment_response["id"]} + except Exception as e: + print(f"ERROR creating payment: {str(e)}") + raise HTTPException(status_code=400, detail=str(e)) + +@router.get("/payment-status/{payment_id}") +async def get_payment_status(payment_id: str, db: Session = Depends(get_db)): + try: + print(f"Checking payment status for ID: {payment_id}") + + # Получение информации о платеже из БД + sale = db.query(Sale).filter(Sale.payment_id == payment_id).first() + + if not sale: + raise HTTPException(status_code=404, detail=f"Платеж с ID {payment_id} не найден") + + # Обновление статуса из YooKassa + yookassa_status = payment_service.get_payment_status(payment_id) + if yookassa_status and "status" in yookassa_status: + # Маппинг статусов YooKassa на наши статусы + if yookassa_status["status"] == "succeeded" or yookassa_status["paid"] == True: + sale.status = "paid" + elif yookassa_status["status"] == "canceled": + sale.status = "canceled" + elif yookassa_status["status"] == "waiting_for_capture": + sale.status = "waiting_for_capture" + # Сохраняем обновленный статус + db.commit() + print(f"Updated payment status from YooKassa: {yookassa_status['status']} -> {sale.status}") + + return {"payment_id": sale.payment_id, "status": sale.status} + except Exception as e: + print(f"ERROR checking payment status: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/notifications/") +async def process_notification(request: Request, db: Session = Depends(get_db)): + try: + # Получаем тело запроса + notification_data = await request.json() + print(f"Received notification: {json.dumps(notification_data, ensure_ascii=False)}") + + # Проверяем тип события + event = notification_data.get("event") + payment_id = notification_data.get("object", {}).get("id") + + if not payment_id: + raise HTTPException(status_code=400, detail="Missing payment ID") + + # Находим платеж в БД + sale = db.query(Sale).filter(Sale.payment_id == payment_id).first() + if not sale: + raise HTTPException(status_code=404, detail=f"Payment {payment_id} not found") + + # Обновляем статус платежа в зависимости от события + if event == "payment.waiting_for_capture": + sale.status = "waiting_for_capture" + elif event == "payment.succeeded": + sale.status = "paid" + elif event == "payment.canceled": + sale.status = "canceled" + else: + print(f"Unknown event: {event}") + + db.commit() + return {"success": True} + except Exception as e: + print(f"ERROR processing notification: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/update-payment-status/") +async def manual_update_payment_status(payment_id: str, new_status: str, db: Session = Depends(get_db)): + try: + sale = db.query(Sale).filter(Sale.payment_id == payment_id).first() + if not sale: + raise HTTPException(status_code=404, detail="Платеж не найден") + + # Обновляем статус + sale.status = new_status + db.commit() + + return {"message": "Статус платежа обновлен", "payment_id": sale.payment_id, "status": sale.status} + except Exception as e: + print(f"ERROR updating payment status: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/user-tickets/{user_id}") +async def get_user_tickets(user_id: int, db: Session = Depends(get_db)): + """Получение всех оплаченных билетов пользователя""" + try: + # Проверяем существование пользователя + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException(status_code=404, detail="Пользователь не найден") + + # Получаем все успешно оплаченные билеты пользователя + sales = db.query(Sale).filter(Sale.user_id == user_id, Sale.status == "paid").all() + + result = [] + for sale in sales: + # Получаем информацию о мероприятии + poster = None + if sale.poster_id: + poster = db.query(Poster).filter(Poster.id == sale.poster_id).first() + + # Формирование даты покупки + purchase_date = sale.date if hasattr(sale, 'date') else datetime.now().isoformat() + + ticket_data = { + "id": sale.id, + "amount": sale.amount, + "description": sale.description, + "payment_id": sale.payment_id, + "purchase_date": purchase_date, + "poster": None + } + + if poster: + # Используем только те поля, которые точно есть в модели Poster + ticket_data["poster"] = { + "id": poster.id, + "title": poster.title, + "date": poster.date + # Убрали поле img, которого нет в модели + } + + result.append(ticket_data) + + return result + except HTTPException as he: + # Пробрасываем дальше HTTPException + raise he + except Exception as e: + print(f"ERROR getting user tickets: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +# Для тестирования, также добавим временный эндпоинт для обновления статуса билета +@router.post("/update-ticket-status/{sale_id}") +async def update_ticket_status(sale_id: int, status: str, db: Session = Depends(get_db)): + """Временный эндпоинт для обновления статуса билета (для тестирования)""" + try: + sale = db.query(Sale).filter(Sale.id == sale_id).first() + if not sale: + raise HTTPException(status_code=404, detail="Билет не найден") + + sale.status = status + db.commit() + + return {"success": True, "message": f"Статус билета с ID {sale_id} обновлен на {status}"} + except Exception as e: + print(f"ERROR updating ticket status: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + diff --git a/backend/core/__pycache__/crypt.cpython-310.pyc b/backend/core/__pycache__/crypt.cpython-310.pyc index db6e4b1..eed55b1 100644 Binary files a/backend/core/__pycache__/crypt.cpython-310.pyc and b/backend/core/__pycache__/crypt.cpython-310.pyc differ diff --git a/backend/core/__pycache__/db.cpython-310.pyc b/backend/core/__pycache__/db.cpython-310.pyc index 5bd4b6b..7d37650 100644 Binary files a/backend/core/__pycache__/db.cpython-310.pyc and b/backend/core/__pycache__/db.cpython-310.pyc differ diff --git a/backend/core/__pycache__/file.cpython-310.pyc b/backend/core/__pycache__/file.cpython-310.pyc index 30db6e6..534af50 100644 Binary files a/backend/core/__pycache__/file.cpython-310.pyc and b/backend/core/__pycache__/file.cpython-310.pyc differ diff --git a/backend/core/__pycache__/pay.cpython-310.pyc b/backend/core/__pycache__/pay.cpython-310.pyc new file mode 100644 index 0000000..468e018 Binary files /dev/null and b/backend/core/__pycache__/pay.cpython-310.pyc differ diff --git a/backend/core/db.py b/backend/core/db.py index 62544ab..5b6da5d 100644 --- a/backend/core/db.py +++ b/backend/core/db.py @@ -3,7 +3,7 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from datetime import datetime -DATABASE_URL = "sqlite:///c:/Users/rbiter/Desktop/AboutMe/backend/database.db" +DATABASE_URL = "sqlite:///./database.db" engine = create_engine(DATABASE_URL) diff --git a/backend/core/pay.py b/backend/core/pay.py new file mode 100644 index 0000000..f9bb2eb --- /dev/null +++ b/backend/core/pay.py @@ -0,0 +1,52 @@ +import uuid +import requests +import base64 + +class YooKassaPayment: + def __init__(self, shop_id: str, api_key: str): + self.shop_id = shop_id + self.api_key = api_key + self.auth_token = base64.b64encode(f'{shop_id}:{api_key}'.encode()).decode() + + def create_payment(self, amount: float, currency: str, description: str, return_url: str, order_id: str = None): + idempotence_key = str(uuid.uuid4()) + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Basic {self.auth_token}', + 'Idempotence-Key': idempotence_key + } + data = { + "amount": { + "value": f"{amount:.2f}", + "currency": currency + }, + "confirmation": { + "type": "redirect", + "return_url": return_url + }, + "capture": True, + "description": description, + "metadata": { + "order_id": order_id or str(uuid.uuid4()) + } + } + response = requests.post("https://api.yookassa.ru/v3/payments", json=data, headers=headers) + if response.status_code in [200, 201]: + return response.json() + else: + raise Exception(f"Ошибка {response.status_code}: {response.text}") + + def get_payment_status(self, payment_id: str): + """Получение актуального статуса платежа из YooKassa API""" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Basic {self.auth_token}' + } + + response = requests.get(f"https://api.yookassa.ru/v3/payments/{payment_id}", headers=headers) + + if response.status_code == 200: + return response.json() + else: + print(f"Error getting payment status: {response.status_code} {response.text}") + return None diff --git a/backend/core/payment/payment_schemas.py b/backend/core/payment/payment_schemas.py new file mode 100644 index 0000000..6b7a3fc --- /dev/null +++ b/backend/core/payment/payment_schemas.py @@ -0,0 +1,24 @@ +from pydantic import BaseModel, constr, condecimal +from typing import Optional + +class PaymentCreateSchema(BaseModel): + amount: condecimal(gt=0) # Amount must be greater than 0 + currency: constr(min_length=3, max_length=3) # Currency code must be 3 characters + description: str + return_url: str + capture: bool = True + metadata: Optional[dict] = None + +class PaymentResponseSchema(BaseModel): + id: str + status: str + confirmation_url: str + +class PaymentStatusSchema(BaseModel): + id: str + status: str + amount: condecimal(gt=0) + currency: constr(min_length=3, max_length=3) # Currency code must be 3 characters + description: str + created_at: str # ISO format date string + confirmation_url: Optional[str] = None \ No newline at end of file diff --git a/backend/database.db b/backend/database.db index 7ec9e2d..90b521a 100644 Binary files a/backend/database.db and b/backend/database.db differ diff --git a/backend/main.py b/backend/main.py index 14bc02a..4bef8b5 100644 --- a/backend/main.py +++ b/backend/main.py @@ -6,6 +6,7 @@ from api.v0.user import router as user_router from fastapi.staticfiles import StaticFiles from api.v0.poster import router as poster_router from api.v0.feedback import router as feedback_router +from api.v0.payments import router as payment_router db.initialize_database() @@ -15,6 +16,7 @@ app = FastAPI() app.include_router(user_router) app.include_router(poster_router) app.include_router(feedback_router) +app.include_router(payment_router) app.mount("/res", StaticFiles(directory="res"), name="res") diff --git a/backend/pay.py b/backend/pay.py deleted file mode 100644 index 3df97ce..0000000 --- a/backend/pay.py +++ /dev/null @@ -1,39 +0,0 @@ -import uuid -import requests -import base64 - -shop_id = '1017909' -api_key = 'test_udCBy7nXqGavVoj3RrzIRXN9UL02_UnF0FBBykxWH60' - -auth_token = base64.b64encode(f'{shop_id}:{api_key}'.encode()).decode() - -idempotence_key = str(uuid.uuid4()) - -headers = { - 'Content-Type': 'application/json', - 'Authorization': f'Basic {auth_token}', - 'Idempotence-Key': idempotence_key -} - -data = { - "amount": { - "value": "1500.00", - "currency": "RUB" - }, - "confirmation": { - "type": "redirect", - "return_url": "https://example.com/return" - }, - "capture": True, - "description": "Зеленый слоник 2", - "metadata": { - "order_id": str(uuid.uuid4()) - } -} - -response = requests.post("https://api.yookassa.ru/v3/payments", json=data, headers=headers) - -if response.status_code in [200, 201]: - print(response.json()["confirmation"]["confirmation_url"]) -else: - print(f"Ошибка {response.status_code}: {response.text}") diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..9fb13cc --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,7 @@ +fastapi +uvicorn +sqlalchemy +pydantic +python-multipart +PyJWT +requests \ No newline at end of file diff --git a/frontend/src/app/payments/page.tsx b/frontend/src/app/payments/page.tsx new file mode 100644 index 0000000..e79e262 --- /dev/null +++ b/frontend/src/app/payments/page.tsx @@ -0,0 +1,172 @@ +'use client'; +import React, { useEffect, useState, CSSProperties } from 'react'; +import Header from '@/components/Header'; +import Fotter from '@/components/Fotter'; +import { useSearchParams, useRouter } from 'next/navigation'; + +const styles = ` +.page-container { + display: flex; + flex-direction: column; + min-height: 100vh; + background: radial-gradient(circle at top right, rgba(30,30,30,0.8) 0%, rgba(15,15,15,0.8) 100%); + position: relative; + overflow-x: hidden; +} + +.page-container::before { + content: ''; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: url('/noise-texture.png'), linear-gradient(145deg, rgba(255,127,39,0.1) 0%, rgba(0,0,0,0) 70%); + opacity: 0.4; + pointer-events: none; + z-index: -1; +} + +.content-container { + flex: 1; + animation: fadeInUp 0.6s ease-out forwards; + width: 100%; +} + +.section-title { + color: #ff7f27; + font-size: 2.5rem; + margin-bottom: 1.5rem; + font-weight: bold; + background: linear-gradient(to right, #ff7f27, #ff5500); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + animation: glowPulse 3s infinite; + text-align: center; + letter-spacing: -0.5px; +} + +.payment-status { + text-align: center; + margin: 2rem auto; + padding: 2rem; + border-radius: 1rem; + background: rgba(0,0,0,0.3); + border: 1px solid rgba(255,127,39,0.2); + color: rgba(255,255,255,0.9); + font-size: 1.2rem; +} + +.success-message { + color: #30d158; +} + +.error-message { + color: #ff453a; +} + +.loading-message { + color: rgba(255,255,255,0.7); +} +`; + +const mainContentStyle: CSSProperties = { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + width: '100%', + maxWidth: '90%', + margin: '0 auto', + flex: 1, + paddingTop: '76px', + paddingBottom: '30px', +}; + +export default function PaymentStatusPage() { + const searchParams = useSearchParams(); + const router = useRouter(); + const paymentId = searchParams.get('payment_id'); + const [status, setStatus] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const checkPaymentStatus = async () => { + // Пытаемся получить payment_id из разных источников + const urlPaymentId = searchParams.get('payment_id'); + const returnPaymentId = searchParams.get('return_payment_id'); + const storedPaymentId = typeof window !== 'undefined' ? localStorage.getItem('current_payment_id') : null; + + // Используем первый доступный ID + const finalPaymentId = urlPaymentId || returnPaymentId || storedPaymentId; + + console.log("Available payment IDs:", { urlPaymentId, returnPaymentId, storedPaymentId }); + + if (!finalPaymentId) { + setStatus('error'); + setError('ID платежа не найден'); + setLoading(false); + return; + } + + try { + console.log("Checking payment status for:", finalPaymentId); + const response = await fetch(`http://localhost:8000/payment-status/${finalPaymentId}`); + + if (!response.ok) { + throw new Error(`Ошибка ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + console.log("Payment status response:", data); + setStatus(data.status); + + // Если платеж завершен, перенаправляем через 5 секунд + if (data.status === 'paid') { + setTimeout(() => { + router.push('/'); // или на страницу с билетами пользователя + }, 5000); + } + } catch (error) { + console.error("Error fetching payment status:", error); + setError('Не удалось получить информацию о платеже'); + setStatus('error'); + } finally { + setLoading(false); + } + }; + + checkPaymentStatus(); + + // Проверяем статус каждые 5 секунд, если платеж ожидает оплаты + const intervalId = setInterval(() => { + if (status === 'pending' || status === 'waiting_for_capture') { + checkPaymentStatus(); + } + }, 5000); + + return () => clearInterval(intervalId); + }, [paymentId, router, status]); + + return ( +
+ +
+
+

Статус платежа

+ {loading ? ( +
Загрузка статуса платежа...
+ ) : status === 'paid' ? ( +
Ваш платеж успешно завершен!
+ ) : status === 'pending' ? ( +
Ваш платеж находится в ожидании подтверждения.
+ ) : status === 'failed' ? ( +
Ваш платеж не удалось завершить.
+ ) : ( +
Произошла ошибка. Платеж не найден.
+ )} +
+ +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/modal/Poster.tsx b/frontend/src/components/modal/Poster.tsx index 05735fc..ab8ee9d 100644 --- a/frontend/src/components/modal/Poster.tsx +++ b/frontend/src/components/modal/Poster.tsx @@ -21,6 +21,7 @@ export default function PosterModal({ data, isOpen, onClose }: PosterModalProps) const [liked, setLiked] = useState(false); const [likesCount, setLikesCount] = useState(data.likes); const [loading, setLoading] = useState(false); + const [paymentLoading, setPaymentLoading] = useState(false); const [message, setMessage] = useState<{text: string, type: 'success' | 'error' | 'info' | null}>({ text: '', type: null @@ -124,6 +125,93 @@ export default function PosterModal({ data, isOpen, onClose }: PosterModalProps) setLoading(false); } }, [data.id, liked, loading]); + + // Функция для обработки платежа + const handlePayment = useCallback(async () => { + if (paymentLoading) return; + + try { + setPaymentLoading(true); + + // Получаем JWT токен из cookie + const token = document.cookie + .split('; ') + .find(row => row.startsWith('JWT_token=')) + ?.split('=')[1]; + + if (!token) { + setMessage({ + text: 'Необходимо авторизоваться для покупки билета', + type: 'error' + }); + setTimeout(() => setMessage({text: '', type: null}), 3000); + return; + } + + try { + // Получаем данные пользователя из JWT + const payload = JSON.parse(atob(token.split('.')[1])); + console.log("JWT payload:", payload); + + // ID пользователя может храниться в разных полях JWT + const userId = payload.sub || payload.user_id || payload.id || 1; + console.log("User ID:", userId); + + // Создание запроса на оплату + const response = await fetch('http://localhost:8000/create-payment/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ + amount: data.price, + currency: 'RUB', + description: `Билет на мероприятие: ${data.title}`, + return_url: `${window.location.origin}/payments`, + user_id: userId, + poster_id: data.id + }) + }); + + console.log("Payment response status:", response.status); + const responseData = await response.json(); + console.log("Payment response data:", responseData); + + if (response.ok && responseData.confirmation_url) { + // Сохраняем payment_id в localStorage перед перенаправлением + if (responseData.payment_id) { + localStorage.setItem('current_payment_id', responseData.payment_id); + } + + // Добавляем payment_id в URL вручную + window.location.href = `${responseData.confirmation_url}&return_payment_id=${responseData.payment_id}`; + } else { + setMessage({ + text: responseData.detail || 'Произошла ошибка при создании платежа', + type: 'error' + }); + setTimeout(() => setMessage({text: '', type: null}), 3000); + } + } catch (parseError) { + console.error('Ошибка при обработке JWT:', parseError); + setMessage({ + text: 'Ошибка авторизации. Пожалуйста, войдите заново.', + type: 'error' + }); + setTimeout(() => setMessage({text: '', type: null}), 3000); + } + } catch (error) { + console.error('Ошибка при создании платежа:', error); + setMessage({ + text: 'Не удалось создать платёж. Попробуйте позже.', + type: 'error' + }); + setTimeout(() => setMessage({text: '', type: null}), 3000); + } finally { + setPaymentLoading(false); + } + }, [data.id, data.price, data.title, paymentLoading]); if (!isOpen) return null; @@ -416,22 +504,48 @@ export default function PosterModal({ data, isOpen, onClose }: PosterModalProps) border: 0, borderRadius: 24, fontWeight: 'bold', - cursor: 'pointer', + cursor: paymentLoading ? 'wait' : 'pointer', color: '#000', transition: 'all 0.3s', - boxShadow: '0 2px 10px rgba(255,127,39,0.4)' + boxShadow: '0 2px 10px rgba(255,127,39,0.4)', + display: 'flex', + alignItems: 'center', + gap: 8, + opacity: paymentLoading ? 0.8 : 1 }} - onClick={() => alert('Функция покупки билетов будет доступна в ближайшее время!')} + onClick={handlePayment} + disabled={paymentLoading} onMouseOver={(e) => { - e.currentTarget.style.transform = 'translateY(-2px)'; - e.currentTarget.style.boxShadow = '0 4px 15px rgba(255,127,39,0.5)'; + if (!paymentLoading) { + e.currentTarget.style.transform = 'translateY(-2px)'; + e.currentTarget.style.boxShadow = '0 4px 15px rgba(255,127,39,0.5)'; + } }} onMouseOut={(e) => { - e.currentTarget.style.transform = 'translateY(0)'; - e.currentTarget.style.boxShadow = '0 2px 10px rgba(255,127,39,0.4)'; + if (!paymentLoading) { + e.currentTarget.style.transform = 'translateY(0)'; + e.currentTarget.style.boxShadow = '0 2px 10px rgba(255,127,39,0.4)'; + } }} > - Купить билет + {paymentLoading ? ( + <> + + + + + Обработка... + + ) : ( + 'Купить билет' + )} diff --git a/frontend/src/components/modal/Profile.tsx b/frontend/src/components/modal/Profile.tsx index 3de8fde..efa6997 100644 --- a/frontend/src/components/modal/Profile.tsx +++ b/frontend/src/components/modal/Profile.tsx @@ -1,7 +1,71 @@ 'use client'; import { CSSProperties, useState, useEffect } from 'react'; -// Улучшенные стили с градиентами и эффектами +// Обновите стили CSS для прокрутки +const styles = ` +@keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes profileCardAppear { + from { opacity: 0; transform: scale(0.95); } + to { opacity: 1; transform: scale(1); } +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Стилизация скроллбара */ +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.3); + border-radius: 10px; + margin: 5px; +} + +::-webkit-scrollbar-thumb { + background: linear-gradient(145deg, #ff7f27, #e96c00); + border-radius: 10px; + border: 2px solid rgba(0, 0, 0, 0.3); +} + +::-webkit-scrollbar-thumb:hover { + background: linear-gradient(145deg, #ff9540, #ff7f27); +} + +::-webkit-scrollbar-corner { + background: transparent; +} + +.loading-spinner { + animation: spin 1s linear infinite; +} + +.tab-content-scroll { + max-height: calc(65vh - 280px); /* Динамическая высота на основе высоты окна */ + overflow-y: auto; + padding-right: 10px; /* Увеличиваем отступ для скроллбара */ + padding-bottom: 20px; /* Добавляем отступ снизу */ + -webkit-overflow-scrolling: touch; /* Плавная прокрутка на iOS */ +} + +.ticket-card { + animation: fadeIn 0.5s forwards; +} + +.ticket-card:last-child { + margin-bottom: 20px; /* Дополнительный отступ для последнего билета */ +} +`; + +// Существующие стили... const wrapper: CSSProperties = { position: 'fixed', inset: 0, @@ -20,15 +84,18 @@ const modal: CSSProperties = { background: 'linear-gradient(145deg, #0a0a0a, #1a1a1a)', border: '1px solid #ff7f27', borderRadius: 24, - padding: 40, - minWidth: 700, // Увеличено для более широкого профиля + padding: '40px 40px 20px 40px', // Уменьшаем нижний отступ + minWidth: 700, maxWidth: '90vw', - width: '100%', // Добавлено для лучшего контроля ширины + width: '100%', color: '#ff7f27', boxShadow: '0 8px 40px rgba(255,127,39,.25), 0 0 0 1px rgba(255,127,39,0.1)', transformOrigin: 'center', transition: 'transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)', animation: 'profileCardAppear 0.4s forwards', + maxHeight: '85vh', + display: 'flex', + flexDirection: 'column', }; const profileHeader: CSSProperties = { @@ -125,9 +192,14 @@ const tabActive: CSSProperties = { background: 'rgba(255,127,39,0.1)', }; +// Обновленный стиль для контента вкладок const tabContent: CSSProperties = { minHeight: 200, - padding: '20px 0', + padding: '20px 0 0 0', // Убираем нижний padding + flex: 1, + overflow: 'hidden', + display: 'flex', + flexDirection: 'column', }; const settingsButton: CSSProperties = { @@ -254,6 +326,20 @@ interface UserData { full_name?: string; // Для обратной совместимости } +interface Ticket { + id: number; + amount: number; + description: string; + payment_id: string; + purchase_date: string; + poster: { + id: number; + title: string; + date: string; + img: string; + } | null; +} + export default function ProfileModal({ user, onClose, @@ -265,22 +351,23 @@ export default function ProfileModal({ }) { const [activeTab, setActiveTab] = useState('tickets'); const [userData, setUserData] = useState(null); - // Removed unused loading state + const [tickets, setTickets] = useState([]); + const [ticketsLoading, setTicketsLoading] = useState(true); // Получение ID пользователя из cookie const userId = document.cookie .split('; ') .find(row => row.startsWith('id=')) ?.split('=')[1]; - - // Получение токена из cookie (removed unused token variable) - // Загрузка полных данных пользователя при открытии профиля + // Загрузка данных пользователя и билетов useEffect(() => { if (userId) { fetchUserData(userId); + fetchUserTickets(userId); } else { setLoading(false); + setTicketsLoading(false); } }, [userId]); @@ -309,6 +396,28 @@ export default function ProfileModal({ } }; + // Новая функция для загрузки билетов + const fetchUserTickets = async (id: string) => { + try { + const response = await fetch(`http://localhost:8000/user-tickets/${id}`, { + method: 'GET', + credentials: 'include', + }); + + if (response.ok) { + const data = await response.json(); + console.log("Получены билеты пользователя:", data); + setTickets(data); + } else { + console.error("Ошибка при получении билетов"); + } + } catch (error) { + console.error("Ошибка запроса билетов:", error); + } finally { + setTicketsLoading(false); + } + }; + // Функция форматирования полного имени const formatName = () => { if (userData) { @@ -328,6 +437,8 @@ export default function ProfileModal({ style={wrapper} onClick={(e) => e.target === e.currentTarget && onClose()} > + {/* Добавляем тег style с нашими стилями */} +