nakama/frontend/src/pages/MonthlyLadder.jsx

299 lines
12 KiB
React
Raw Normal View History

2025-12-07 02:20:45 +00:00
import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { ChevronLeft, Info, Gift, Trophy, Star } from 'lucide-react'
import { getLadderTop } from '../utils/api'
import { hapticFeedback } from '../utils/telegram'
import './MonthlyLadder.css'
export default function MonthlyLadder({ user }) {
const navigate = useNavigate()
const [topUsers, setTopUsers] = useState([])
const [currentUser, setCurrentUser] = useState(null)
const [currentUserRank, setCurrentUserRank] = useState(null)
const [loading, setLoading] = useState(true)
const [showInfo, setShowInfo] = useState(false)
const [timeLeft, setTimeLeft] = useState({ days: 0, hours: 0, minutes: 0, seconds: 0 })
useEffect(() => {
loadLadder()
updateCountdown()
const interval = setInterval(updateCountdown, 1000)
return () => clearInterval(interval)
}, [])
const updateCountdown = () => {
// Получить текущее московское время
const getMoscowTime = () => {
const now = new Date()
// Москва = UTC+3
const moscowOffset = 3 * 60 * 60 * 1000 // 3 часа в миллисекундах
const utcTime = now.getTime() + (now.getTimezoneOffset() * 60 * 1000)
return new Date(utcTime + moscowOffset)
}
// Получить новогоднюю дату по московскому времени (1 января следующего года, 00:00 MSK)
const getNewYearMoscow = () => {
const moscowNow = getMoscowTime()
const year = moscowNow.getFullYear() + 1
// Создаем дату 1 января следующего года в UTC, затем вычитаем смещение
const moscowNewYear = new Date(Date.UTC(year, 0, 1, 0, 0, 0, 0))
const moscowOffset = 3 * 60 * 60 * 1000
return new Date(moscowNewYear.getTime() - moscowOffset)
}
const now = getMoscowTime()
const newYear = getNewYearMoscow()
const diff = newYear.getTime() - now.getTime()
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
const seconds = Math.floor((diff % (1000 * 60)) / 1000)
setTimeLeft({ days, hours, minutes, seconds })
}
const loadLadder = async () => {
try {
setLoading(true)
const data = await getLadderTop(5)
setTopUsers(data.topUsers || [])
setCurrentUser(data.currentUser)
setCurrentUserRank(data.currentUserRank)
} catch (error) {
console.error('Ошибка загрузки ладдера:', error)
} finally {
setLoading(false)
}
}
const getRankIcon = (rank) => {
switch (rank) {
case 1:
return <Trophy size={24} className="rank-icon gold" />
case 2:
return <Trophy size={24} className="rank-icon silver" />
case 3:
return <Trophy size={24} className="rank-icon bronze" />
default:
return <span className="rank-number">{rank}</span>
}
}
2025-12-07 02:36:30 +00:00
const getPrize = (rank) => {
switch (rank) {
case 1:
return '$50'
case 2:
return '$30'
case 3:
return '$15'
case 4:
return '$5'
case 5:
return '$5'
default:
return null
}
}
2025-12-07 02:20:45 +00:00
const formatTickets = (tickets) => {
return tickets?.toLocaleString('ru-RU') || '0'
}
return (
<div className="ladder-page">
{/* Хедер */}
<div className="ladder-header">
<button className="back-btn" onClick={() => navigate(-1)}>
<ChevronLeft size={24} />
</button>
<h1>Monthly Ladder</h1>
<div style={{ width: 44 }} />
</div>
{/* Новогодний декор */}
<div className="new-year-decorations">
<div className="snowflake"></div>
<div className="snowflake"></div>
<div className="snowflake"></div>
<div className="snowflake"></div>
<div className="snowflake"></div>
<div className="snowflake"></div>
</div>
{/* Отсчет до нового года */}
2025-12-07 02:27:23 +00:00
<div className="countdown-card">
2025-12-07 02:20:45 +00:00
<div className="countdown-title">
<Gift size={24} className="gift-icon" />
<h2>До Нового Года</h2>
</div>
<div className="countdown-timer">
<div className="countdown-item">
<span className="countdown-value">{timeLeft.days}</span>
<span className="countdown-label">дней</span>
</div>
<div className="countdown-separator">:</div>
<div className="countdown-item">
<span className="countdown-value">{String(timeLeft.hours).padStart(2, '0')}</span>
<span className="countdown-label">часов</span>
</div>
<div className="countdown-separator">:</div>
<div className="countdown-item">
<span className="countdown-value">{String(timeLeft.minutes).padStart(2, '0')}</span>
<span className="countdown-label">минут</span>
</div>
<div className="countdown-separator">:</div>
<div className="countdown-item">
<span className="countdown-value">{String(timeLeft.seconds).padStart(2, '0')}</span>
<span className="countdown-label">секунд</span>
</div>
</div>
<p className="countdown-slogan">Ваши посты, ваши арты, ваша слава. Остальное потом.</p>
</div>
{/* Топ 5 пользователей */}
2025-12-07 02:27:23 +00:00
<div className="ladder-top">
2025-12-07 02:20:45 +00:00
<div className="ladder-top-header">
2025-12-07 02:36:30 +00:00
<div className="ladder-top-title">
<h2>Топ 5</h2>
<p className="ladder-prizes">Призы: 1 место - $50, 2 место - $30, 3 место - $15, 4-5 места - $5</p>
</div>
2025-12-07 02:20:45 +00:00
<button
className="info-btn"
onClick={() => {
setShowInfo(true)
hapticFeedback('light')
}}
>
<Info size={20} />
<span>За что начисляются баллы</span>
</button>
</div>
{loading ? (
<div className="loading-state">
<div className="spinner" />
</div>
) : (
<div className="top-users-list">
{topUsers.map((topUser, index) => {
const isCurrentUser = user && (topUser._id === user.id || topUser._id?.toString() === user.id?.toString())
2025-12-07 02:36:30 +00:00
const prize = getPrize(topUser.rank)
2025-12-07 02:20:45 +00:00
return (
<div
key={topUser._id}
className={`top-user-item ${isCurrentUser ? 'current-user' : ''}`}
>
<div className="user-rank">
{getRankIcon(topUser.rank)}
</div>
<img
src={topUser.photoUrl || '/default-avatar.png'}
alt={topUser.username}
className="user-avatar"
/>
<div className="user-info">
<div className="user-name">
{topUser.firstName || topUser.username}
{isCurrentUser && <Star size={16} className="current-badge" />}
</div>
2025-12-07 02:36:30 +00:00
</div>
<div className="user-stats">
<span className="user-tickets">{formatTickets(topUser.tickets)} билетов</span>
{prize && <span className="user-prize">{prize}</span>}
2025-12-07 02:20:45 +00:00
</div>
</div>
)
})}
</div>
)}
</div>
{/* Текущий пользователь (если не в топе) */}
{currentUser && currentUserRank > 5 && (
2025-12-07 02:27:23 +00:00
<div className="current-user-card">
2025-12-07 02:20:45 +00:00
<h3>Ваша позиция</h3>
<div className="current-user-item">
<div className="user-rank">
<span className="rank-number">{currentUserRank}</span>
</div>
<img
src={currentUser.photoUrl || '/default-avatar.png'}
alt={currentUser.username}
className="user-avatar"
/>
<div className="user-info">
<div className="user-name">
{currentUser.firstName || currentUser.username}
<Star size={16} className="current-badge" />
</div>
2025-12-07 02:36:30 +00:00
</div>
<div className="user-stats">
<span className="user-tickets">{formatTickets(currentUser.tickets)} билетов</span>
2025-12-07 02:20:45 +00:00
</div>
</div>
</div>
)}
{/* Модальное окно с информацией */}
{showInfo && (
<div className="info-modal-overlay" onClick={() => setShowInfo(false)}>
2025-12-07 02:36:30 +00:00
<div className="info-modal" onClick={(e) => e.stopPropagation()}>
2025-12-07 02:20:45 +00:00
<div className="info-modal-header">
<h2>За что начисляются баллы</h2>
<button className="close-btn" onClick={() => setShowInfo(false)}>×</button>
</div>
<div className="info-modal-content">
<div className="info-section">
<h3>1. Посты</h3>
<p>+15 баллов за создание поста</p>
<p className="info-limit">Лимит: 5 постов в день</p>
</div>
<div className="info-section">
<h3>2. Лайки</h3>
<p><strong>Ставишь лайки:</strong> +1 балл за лайк</p>
<p className="info-limit">Лимит: 50 в день</p>
<p><strong>Получаешь лайки:</strong> +2 балла за лайк под твоей записью</p>
<p className="info-limit">Лимит учёта: 100 лайков в день</p>
</div>
<div className="info-section">
<h3>3. Комментарии</h3>
<p><strong>Пишешь комментарии:</strong> +4 балла за комментарий длиной 10+ символов</p>
<p className="info-limit">Лимит: 20 комментариев в день</p>
<p><strong>Получаешь комментарии:</strong> +6 баллов за комментарий под твоим постом</p>
</div>
<div className="info-section">
<h3>4. Рефералы</h3>
<p>+100 баллов за одного валидного реферала</p>
<p className="info-limit">Лимит: 3 реферала в день</p>
</div>
<div className="info-section">
<h3>5. Ваше творчество (арты)</h3>
<p><strong>Публикация:</strong> +40 баллов за арт, прошедший модерацию</p>
<p className="info-limit">Лимит: 1 арт в день / 5 в неделю</p>
<p><strong>Реакции на арт:</strong></p>
<p>+8 баллов за лайк под артом</p>
<p>+12 баллов за комментарий под артом (1 комментарий от одного человека в сутки)</p>
<p className="info-limit">Лимит: до 100 баллов в сутки с реакций на один арт</p>
</div>
<div className="info-section anti-fraud">
2025-12-07 02:36:30 +00:00
<h3>Немного правил</h3>
2025-12-07 02:20:45 +00:00
<p>Лайки/комменты от аккаунтов младше 24 часов не считаем</p>
<p>Комменты &lt;10 символов = 0 баллов</p>
<p>Ограничение на баллы по входящим реакциям, чтобы боты не устроили ферму</p>
</div>
</div>
</div>
</div>
)}
</div>
)
}