import React, { useEffect, useState, useCallback, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import * as Kubikon3DApi from '../../api/Kubikon3DService'; /** * Форматирует время в мс → "1:23.45" (мин:сек.сотые) или "23.45" если меньше минуты. */ export function formatTimeMs(ms) { if (ms == null || !Number.isFinite(ms)) return '—'; const totalSec = ms / 1000; const mm = Math.floor(totalSec / 60); const sec = totalSec - mm * 60; if (mm > 0) { return mm + ':' + sec.toFixed(2).padStart(5, '0'); } return sec.toFixed(2); } const RANK_BADGES = ['🥇', '🥈', '🥉', '4️⃣', '5️⃣']; /** * KubikonLeaderboard — таблица топ-N рекордов прохождения игры. * * Props: * projectId — id проекта Кубикон 3D * limit — топ N (default 5) * currentTimeMs — текущее время игрока (показывается отдельной строкой) * currentUserId — id текущего игрока (выделить его строку) * onClose — если задан, показывается крестик в углу для скрытия * compact — если true, компактный режим (для оверлея в игре) * refreshKey — менять чтобы перезагрузить (например при submit) * style, className — для оборачивания */ export default function KubikonLeaderboard({ projectId, limit = 5, currentTimeMs = null, currentUserId = null, onClose = null, compact = false, refreshKey = 0, clickable = true, onLoaded = null, style = {}, className = '', }) { const navigate = useNavigate(); const [records, setRecords] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // onLoaded храним в ref чтобы не плодить fetchData при каждом рендере // родителя (если он не оборачивает onLoaded в useCallback). Без этого // компонент попадал в бесконечный цикл fetch. const onLoadedRef = useRef(onLoaded); useEffect(() => { onLoadedRef.current = onLoaded; }, [onLoaded]); const fetchData = useCallback(async () => { if (!projectId) return; setLoading(true); setError(null); try { const r = await Kubikon3DApi.getLeaderboard(projectId, limit); const list = r.data?.records || []; setRecords(list); const cb = onLoadedRef.current; if (cb) try { cb(list); } catch (e) {} } catch (e) { setError(e?.message || 'Ошибка загрузки'); } finally { setLoading(false); } }, [projectId, limit]); useEffect(() => { fetchData(); }, [fetchData, refreshKey]); const goProfile = (uid) => { if (!clickable) return; if (!uid) return; // Профили живут на rublox.pro, не в плеере. Внешний редирект. window.location.assign(`https://rublox.pro/app/profile/${uid}`); }; // Палитра — берём «золотистую» гамму с лёгким бирюзовым акцентом (соответствует Кубикону) const C = { bg: compact ? 'rgba(10, 14, 26, 0.92)' : 'rgba(20, 24, 45, 0.96)', bgRow: 'rgba(15, 19, 38, 0.55)', bgRowAlt: 'rgba(15, 19, 38, 0.30)', bgRowMe: 'rgba(255, 215, 0, 0.16)', border: 'rgba(255, 215, 0, 0.45)', borderSoft:'rgba(255, 255, 255, 0.10)', gold: '#ffd700', goldDim: '#caa01c', text: '#f1f5fb', muted: 'rgba(241, 245, 251, 0.55)', accent: '#3357ff', success: '#22d97a', }; return (
{/* Заголовок */}
🏆 Топ-{limit}
{onClose && ( )}
{/* Тело */} {loading ? (
Загружаю…
) : error ? (
{error}
) : records.length === 0 ? (
Пока нет рекордов.
Стань первым!
) : (
{records.map((r, idx) => { const isMe = currentUserId && r.user_id === currentUserId; return (
goProfile(r.user_id)} style={{ display: 'grid', gridTemplateColumns: '32px 1fr auto', alignItems: 'center', gap: 8, padding: '8px 10px', borderRadius: 10, background: isMe ? C.bgRowMe : idx % 2 === 0 ? C.bgRow : C.bgRowAlt, border: isMe ? `1px solid ${C.gold}` : `1px solid ${C.borderSoft}`, cursor: clickable ? 'pointer' : 'default', transition: 'transform 0.12s ease, background 0.12s ease', }} onMouseEnter={e => { if (!clickable) return; e.currentTarget.style.transform = 'translateX(2px)'; e.currentTarget.style.background = isMe ? 'rgba(255,215,0,0.24)' : 'rgba(51,87,255,0.18)'; }} onMouseLeave={e => { if (!clickable) return; e.currentTarget.style.transform = 'translateX(0)'; e.currentTarget.style.background = isMe ? C.bgRowMe : idx % 2 === 0 ? C.bgRow : C.bgRowAlt; }} >
{RANK_BADGES[idx] || (idx + 1)}
{r.username || `id ${r.user_id}`} {isMe && (ты)}
{formatTimeMs(r.time_ms)}
); })}
)} {/* Текущее время игрока (если идёт прохождение) */} {currentTimeMs != null && currentTimeMs > 0 && (
⏱️ Твоё время {formatTimeMs(currentTimeMs)}
)}
); }