nakama/frontend/src/pages/MonthlyLadder.jsx

299 lines
12 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

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>
}
}
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
}
}
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>
{/* Отсчет до нового года */}
<div className="countdown-card">
<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 пользователей */}
<div className="ladder-top">
<div className="ladder-top-header">
<div className="ladder-top-title">
<h2>Топ 5</h2>
<p className="ladder-prizes">Призы: 1 место - $50, 2 место - $30, 3 место - $15, 4-5 места - $5</p>
</div>
<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())
const prize = getPrize(topUser.rank)
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>
</div>
<div className="user-stats">
<span className="user-tickets">{formatTickets(topUser.tickets)} билетов</span>
{prize && <span className="user-prize">{prize}</span>}
</div>
</div>
)
})}
</div>
)}
</div>
{/* Текущий пользователь (если не в топе) */}
{currentUser && currentUserRank > 5 && (
<div className="current-user-card">
<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>
</div>
<div className="user-stats">
<span className="user-tickets">{formatTickets(currentUser.tickets)} билетов</span>
</div>
</div>
</div>
)}
{/* Модальное окно с информацией */}
{showInfo && (
<div className="info-modal-overlay" onClick={() => setShowInfo(false)}>
<div className="info-modal" onClick={(e) => e.stopPropagation()}>
<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">
<h3>Немного правил</h3>
<p>Лайки/комменты от аккаунтов младше 24 часов не считаем</p>
<p>Комменты &lt;10 символов = 0 баллов</p>
<p>Ограничение на баллы по входящим реакциям, чтобы боты не устроили ферму</p>
</div>
</div>
</div>
</div>
)}
</div>
)
}