299 lines
12 KiB
JavaScript
299 lines
12 KiB
JavaScript
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>Комменты <10 символов = 0 баллов</p>
|
||
<p>Ограничение на баллы по входящим реакциям, чтобы боты не устроили ферму</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|