2025-06-10 18:21:58 +03:00

556 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useState, useCallback, useEffect } from 'react';
import { format } from 'date-fns';
import { ru } from 'date-fns/locale';
type PosterModalProps = {
data: {
id: number;
img: string;
title: string;
likes: number;
desc: string;
price: number;
date: string;
};
isOpen: boolean;
onClose: () => void;
};
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
});
// Блокировка прокрутки страницы при открытом модальном окне
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [isOpen]);
// Обработка клавиши Escape
useEffect(() => {
const handleEscKey = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isOpen) {
onClose();
}
};
window.addEventListener('keydown', handleEscKey);
return () => {
window.removeEventListener('keydown', handleEscKey);
};
}, [isOpen, onClose]);
// Функция для форматирования даты
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
return format(date, "d MMMM yyyy 'г. в' HH:mm", { locale: ru });
};
// Функция обработки лайков
const handleLike = useCallback(async () => {
if (loading) return;
try {
setLoading(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;
}
const formData = new FormData();
formData.append('poster_id', String(data.id));
formData.append('like', liked ? '-1' : '1');
const response = await fetch('http://localhost:8000/setlike', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
body: formData,
credentials: 'include'
});
const responseData = await response.json();
if (response.ok) {
setLikesCount(responseData.poster.like);
setMessage({
text: responseData.message,
type: 'success'
});
if (responseData.message !== "Вы уже оценили этот постер." &&
responseData.message !== "Вы еще не ставили лайк этому постеру.") {
setLiked(!liked);
}
} else {
setMessage({
text: responseData.detail || 'Произошла ошибка при оценке мероприятия',
type: 'error'
});
}
setTimeout(() => setMessage({text: '', type: null}), 3000);
} catch (error) {
console.error('Ошибка при отправке лайка:', error);
setMessage({
text: 'Не удалось обработать запрос. Попробуйте позже.',
type: 'error'
});
setTimeout(() => setMessage({text: '', type: null}), 3000);
} finally {
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;
// Расширенные стили с усиленными эффектами размытия и поддержкой кросс-браузерности
const styles = `
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes heartbeat {
0% { transform: scale(1); }
15% { transform: scale(1.25); }
25% { transform: scale(1.15); }
35% { transform: scale(1.25); }
100% { transform: scale(1); }
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-spinner {
animation: spin 1s linear infinite;
}
/* Улучшенная поддержка эффекта размытия для разных браузеров */
@supports (-webkit-backdrop-filter: none) or (backdrop-filter: none) {
.modal-backdrop {
-webkit-backdrop-filter: blur(25px) saturate(120%);
backdrop-filter: blur(25px) saturate(120%);
background-color: rgba(0,0,0,.65);
}
}
@supports not ((-webkit-backdrop-filter: none) or (backdrop-filter: none)) {
.modal-backdrop {
background-color: rgba(0,0,0,.9);
}
}
`;
const modalContainerStyle = {
position: 'fixed' as const,
top: 0,
left: 0,
right: 0,
bottom: 0,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 2001, // Повышенный z-index
pointerEvents: 'none' as const,
};
return (
<>
<style>{styles}</style>
<div
className="modal-backdrop"
style={{
position: 'fixed',
inset: 0,
background: 'rgba(0,0,0,.65)',
backdropFilter: 'blur(25px) saturate(120%)',
WebkitBackdropFilter: 'blur(25px) saturate(120%)',
zIndex: 2000, // Повышенный z-index
willChange: 'backdrop-filter',
pointerEvents: 'auto',
}}
onClick={onClose}
/>
<div style={modalContainerStyle}>
<div
onClick={e => e.stopPropagation()}
style={{
display: 'flex',
background: 'rgba(15,15,15,0.35)',
border: '1px solid rgba(255,127,39,0.5)',
borderRadius: 24,
overflow: 'hidden',
boxShadow: '0 15px 35px rgba(0,0,0,0.2)',
maxWidth: '90vw',
maxHeight: '85vh',
width: 'auto',
height: 'auto',
position: 'relative',
animation: 'fadeIn 0.3s forwards',
pointerEvents: 'auto',
}}
>
<button
onClick={onClose}
style={{
position: 'absolute',
top: 16,
right: 16,
background: 'rgba(0,0,0,0.5)',
border: '1px solid rgba(255,127,39,0.4)',
color: '#fff',
width: 32,
height: 32,
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
transition: 'all 0.2s',
zIndex: 10,
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6L18 18" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
<div style={{
width: '40%',
height: '85vh',
maxHeight: '85vh',
flexShrink: 0,
position: 'relative',
overflow: 'hidden',
borderRight: '1px solid rgba(255,127,39,0.3)',
}}>
<img
src={data.img}
alt={data.title}
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
objectPosition: 'center',
display: 'block',
}}
/>
</div>
<div style={{
width: '60%',
padding: 40,
display: 'flex',
flexDirection: 'column',
color: '#fff',
}}>
<h1 style={{
color: '#ff7f27',
fontSize: 28,
fontWeight: 'bold',
marginBottom: 16,
textShadow: '0 2px 10px rgba(255,127,39,0.3)'
}}>
{data.title}
</h1>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 20,
color: 'rgba(255,255,255,0.7)',
padding: '8px 0',
borderBottom: '1px solid rgba(255,127,39,0.2)'
}}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 6,
position: 'relative'
}}>
<button
onClick={handleLike}
disabled={loading}
style={{
background: 'transparent',
border: 'none',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
padding: 5,
transition: 'all 0.2s'
}}
title={liked ? 'Убрать оценку' : 'Оценить мероприятие'}
>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M7 22H4C3.46957 22 2.96086 21.7893 2.58579 21.4142C2.21071 21.0391 2 20.5304 2 20V13C2 12.4696 2.21071 11.9609 2.58579 11.5858C2.96086 11.2107 3.46957 11 4 11H7M14 9V5C14 4.20435 13.6839 3.44129 13.1213 2.87868C12.5587 2.31607 11.7956 2 11 2L7 11V22H18.28C18.7623 22.0055 19.2304 21.8364 19.5979 21.524C19.9654 21.2116 20.2077 20.7769 20.28 20.3L21.66 11.3C21.7035 11.0134 21.6842 10.7207 21.6033 10.4423C21.5225 10.1638 21.3821 9.90629 21.1919 9.68751C21.0016 9.46873 20.7661 9.29393 20.5016 9.17522C20.2371 9.0565 19.9499 8.99672 19.66 9H14Z"
stroke={liked ? "#ff7f27" : "rgba(255,255,255,0.7)"}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
fill={liked ? "#ff7f27" : "none"}
/>
</svg>
</button>
<span style={{ color: liked ? '#ff7f27' : 'rgba(255,255,255,0.7)' }}>
{likesCount}
</span>
{loading && (
<span style={{ marginLeft: 5 }}>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="loading-spinner"
>
<circle cx="12" cy="12" r="10" stroke="rgba(255,127,39,0.3)" strokeWidth="2" />
<path d="M12 2C6.47715 2 2 6.47715 2 12" stroke="#ff7f27" strokeWidth="2" />
</svg>
</span>
)}
{message.text && (
<div style={{
position: 'absolute',
left: '100%',
marginLeft: 16,
whiteSpace: 'nowrap',
fontSize: 14,
padding: '6px 12px',
borderRadius: 8,
boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
zIndex: 5,
maxWidth: 250,
animation: 'fadeIn 0.2s forwards',
backgroundColor: message.type === 'error' ? 'rgba(255,60,60,0.15)' :
message.type === 'success' ? 'rgba(39,255,127,0.15)' : 'rgba(255,127,39,0.15)',
color: message.type === 'error' ? '#ff5252' :
message.type === 'success' ? '#57ff8a' : '#ff7f27',
border: `1px solid ${message.type === 'error' ? 'rgba(255,60,60,0.3)' :
message.type === 'success' ? 'rgba(39,255,127,0.3)' : 'rgba(255,127,39,0.3)'}`
}}>
{message.text}
</div>
)}
</div>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 6,
fontSize: 14
}}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 4H5C3.89543 4 3 4.89543 3 6V20C3 21.1046 3.89543 22 5 22H19C20.1046 22 21 21.1046 21 20V6C21 4.89543 20.1046 4 19 4Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M16 2V6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M8 2V6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M3 10H21" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
{formatDate(data.date)}
</div>
</div>
<div style={{
lineHeight: 1.7,
fontSize: 16,
color: 'rgba(255,255,255,0.9)',
margin: '20px 0 30px',
flex: 1
}}>
{data.desc}
</div>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: 'auto',
paddingTop: 16,
borderTop: '1px solid rgba(255,127,39,0.2)'
}}>
<div style={{
color: '#ff7f27',
fontSize: 24,
fontWeight: 'bold',
display: 'flex',
alignItems: 'center',
gap: 15
}}>
{data.price.toLocaleString('ru-RU', { style: 'currency', currency: 'RUB' })}
</div>
<button
style={{
padding: '12px 24px',
background: 'linear-gradient(145deg, #ff7f27, #ff5500)',
border: 0,
borderRadius: 24,
fontWeight: 'bold',
cursor: paymentLoading ? 'wait' : 'pointer',
color: '#000',
transition: 'all 0.3s',
boxShadow: '0 2px 10px rgba(255,127,39,0.4)',
display: 'flex',
alignItems: 'center',
gap: 8,
opacity: paymentLoading ? 0.8 : 1
}}
onClick={handlePayment}
disabled={paymentLoading}
onMouseOver={(e) => {
if (!paymentLoading) {
e.currentTarget.style.transform = 'translateY(-2px)';
e.currentTarget.style.boxShadow = '0 4px 15px rgba(255,127,39,0.5)';
}
}}
onMouseOut={(e) => {
if (!paymentLoading) {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 2px 10px rgba(255,127,39,0.4)';
}
}}
>
{paymentLoading ? (
<>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="loading-spinner"
>
<circle cx="12" cy="12" r="10" stroke="rgba(0,0,0,0.3)" strokeWidth="2.5" />
<path d="M12 2C6.47715 2 2 6.47715 2 12" stroke="#000" strokeWidth="2.5" />
</svg>
Обработка...
</>
) : (
'Купить билет'
)}
</button>
</div>
</div>
</div>
</div>
</>
);
}