This commit is contained in:
Joan
2025-11-27 16:27:01 +01:00
parent 33cc9586c2
commit 81f8912059
304 changed files with 56149 additions and 10122 deletions

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import GameHeader from './GameHeader';
import api from '../services/api';
import './Leaderboards.css';
import './Game.css';
@@ -53,19 +53,11 @@ export default function Leaderboards() {
setError(null);
try {
const token = localStorage.getItem('authToken');
const response = await fetch(`/api/leaderboard/${statName}?limit=100`, {
headers: {
'Authorization': `Bearer ${token}`,
},
const response = await api.get(`/api/leaderboard/${statName}`, {
params: { limit: 100 }
});
if (!response.ok) {
throw new Error('Failed to fetch leaderboard');
}
const data = await response.json();
setLeaderboard(data.leaderboard || []);
setLeaderboard(response.data.leaderboard || []);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
@@ -97,11 +89,11 @@ export default function Leaderboards() {
};
return (
<div className="game-container">
<GameHeader className={mobileHeaderOpen ? 'open' : ''} />
<div className="leaderboards-container">
{/* Game Header is now in GameLayout */}
{/* Mobile Header Toggle */}
<button
<button
className="mobile-header-toggle"
onClick={() => setMobileHeaderOpen(!mobileHeaderOpen)}
>
@@ -110,153 +102,70 @@ export default function Leaderboards() {
<main className="game-main">
<div className="leaderboards-container">
<div className="stat-selector">
<h3>Select Statistic</h3>
<div className={`stat-options ${statDropdownOpen ? 'expanded' : ''}`}>
{STAT_OPTIONS.map((stat) => (
<button
key={stat.key}
className={`stat-option ${selectedStat.key === stat.key ? 'active' : ''}`}
onClick={() => {
if (selectedStat.key === stat.key) {
// Toggle dropdown when clicking active item
setStatDropdownOpen(!statDropdownOpen);
} else {
// Select new stat and close dropdown
setSelectedStat(stat);
setStatDropdownOpen(false);
}
}}
style={{
borderColor: selectedStat.key === stat.key ? stat.color : 'rgba(255, 255, 255, 0.2)',
}}
>
<span className="stat-icon">{stat.icon}</span>
<span className="stat-label">{stat.label}</span>
</button>
))}
</div>
</div>
<div className="leaderboard-content">
<div
className={`leaderboard-title ${statDropdownOpen ? 'dropdown-open' : ''}`}
style={{ borderColor: selectedStat.color }}
>
<div
className="title-left clickable-title"
onClick={() => setStatDropdownOpen(!statDropdownOpen)}
>
<span className="title-icon">{selectedStat.icon}</span>
<h2>{selectedStat.label}</h2>
<span className="dropdown-arrow">{statDropdownOpen ? '▲' : '▼'}</span>
</div>
{/* Dropdown options */}
{statDropdownOpen && (
<div className="title-dropdown">
{STAT_OPTIONS.filter(stat => stat.key !== selectedStat.key).map((stat) => (
<button
key={stat.key}
className="title-dropdown-option"
onClick={() => {
<div className="stat-selector">
<h3>Select Statistic</h3>
<div className={`stat-options ${statDropdownOpen ? 'expanded' : ''}`}>
{STAT_OPTIONS.map((stat) => (
<button
key={stat.key}
className={`stat-option ${selectedStat.key === stat.key ? 'active' : ''}`}
onClick={() => {
if (selectedStat.key === stat.key) {
// Toggle dropdown when clicking active item
setStatDropdownOpen(!statDropdownOpen);
} else {
// Select new stat and close dropdown
setSelectedStat(stat);
setStatDropdownOpen(false);
}}
>
<span className="stat-icon">{stat.icon}</span>
<span className="stat-label">{stat.label}</span>
</button>
))}
</div>
)}
{!loading && !error && leaderboard.length > ITEMS_PER_PAGE && (
<div className="pagination pagination-top">
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="pagination-btn"
}
}}
style={{
borderColor: selectedStat.key === stat.key ? stat.color : 'rgba(255, 255, 255, 0.2)',
}}
>
<span className="stat-icon">{stat.icon}</span>
<span className="stat-label">{stat.label}</span>
</button>
<span className="pagination-info">
{currentPage} / {Math.ceil(leaderboard.length / ITEMS_PER_PAGE)}
</span>
<button
onClick={() => setCurrentPage(p => Math.min(Math.ceil(leaderboard.length / ITEMS_PER_PAGE), p + 1))}
disabled={currentPage >= Math.ceil(leaderboard.length / ITEMS_PER_PAGE)}
className="pagination-btn"
>
</button>
</div>
)}
))}
</div>
</div>
{loading && (
<div className="leaderboard-loading">
<div className="spinner"></div>
<p>Loading leaderboard...</p>
</div>
)}
{error && (
<div className="leaderboard-error">
<p> {error}</p>
<button onClick={() => fetchLeaderboard(selectedStat.key)}>Retry</button>
</div>
)}
{!loading && !error && leaderboard.length === 0 && (
<div className="leaderboard-empty">
<p>📊 No data available yet</p>
</div>
)}
{!loading && !error && leaderboard.length > 0 && (
<>
<div className="leaderboard-table">
<div className="table-header">
<div className="col-rank">Rank</div>
<div className="col-player">Player</div>
<div className="col-level">Level</div>
<div className="col-value">Value</div>
</div>
{leaderboard
.slice((currentPage - 1) * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE)
.map((entry, index) => {
const rank = (currentPage - 1) * ITEMS_PER_PAGE + index + 1;
return (
<div
key={entry.player_id}
className={`table-row ${getRankClass(rank)}`}
onClick={() => navigate(`/profile/${entry.player_id}`)}
>
<div className="col-rank">
<span className="rank-badge">{getRankBadge(rank)}</span>
</div>
<div className="col-player">
<div className="player-name">{entry.name}</div>
<div className="player-username">@{entry.username}</div>
</div>
<div className="col-level">
<span className="level-badge">Lv {entry.level}</span>
</div>
<div className="col-value">
<span className="stat-value" style={{ color: selectedStat.color }}>
{formatStatValue(entry.value, selectedStat.key)}
</span>
</div>
</div>
);
})}
<div className="leaderboard-content">
<div
className={`leaderboard-title ${statDropdownOpen ? 'dropdown-open' : ''}`}
style={{ borderColor: selectedStat.color }}
>
<div
className="title-left clickable-title"
onClick={() => setStatDropdownOpen(!statDropdownOpen)}
>
<span className="title-icon">{selectedStat.icon}</span>
<h2>{selectedStat.label}</h2>
<span className="dropdown-arrow">{statDropdownOpen ? '▲' : '▼'}</span>
</div>
{Math.ceil(leaderboard.length / ITEMS_PER_PAGE) > 1 && (
<div className="pagination pagination-bottom">
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
{/* Dropdown options */}
{statDropdownOpen && (
<div className="title-dropdown">
{STAT_OPTIONS.filter(stat => stat.key !== selectedStat.key).map((stat) => (
<button
key={stat.key}
className="title-dropdown-option"
onClick={() => {
setSelectedStat(stat);
setStatDropdownOpen(false);
}}
>
<span className="stat-icon">{stat.icon}</span>
<span className="stat-label">{stat.label}</span>
</button>
))}
</div>
)}
{!loading && !error && leaderboard.length > ITEMS_PER_PAGE && (
<div className="pagination pagination-top">
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="pagination-btn"
>
@@ -265,8 +174,8 @@ export default function Leaderboards() {
<span className="pagination-info">
{currentPage} / {Math.ceil(leaderboard.length / ITEMS_PER_PAGE)}
</span>
<button
onClick={() => setCurrentPage(p => Math.min(Math.ceil(leaderboard.length / ITEMS_PER_PAGE), p + 1))}
<button
onClick={() => setCurrentPage(p => Math.min(Math.ceil(leaderboard.length / ITEMS_PER_PAGE), p + 1))}
disabled={currentPage >= Math.ceil(leaderboard.length / ITEMS_PER_PAGE)}
className="pagination-btn"
>
@@ -274,9 +183,92 @@ export default function Leaderboards() {
</button>
</div>
)}
</>
)}
</div>
</div>
{loading && (
<div className="leaderboard-loading">
<div className="spinner"></div>
<p>Loading leaderboard...</p>
</div>
)}
{error && (
<div className="leaderboard-error">
<p> {error}</p>
<button onClick={() => fetchLeaderboard(selectedStat.key)}>Retry</button>
</div>
)}
{!loading && !error && leaderboard.length === 0 && (
<div className="leaderboard-empty">
<p>📊 No data available yet</p>
</div>
)}
{!loading && !error && leaderboard.length > 0 && (
<>
<div className="leaderboard-table">
<div className="table-header">
<div className="col-rank">Rank</div>
<div className="col-player">Player</div>
<div className="col-level">Level</div>
<div className="col-value">Value</div>
</div>
{leaderboard
.slice((currentPage - 1) * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE)
.map((entry, index) => {
const rank = (currentPage - 1) * ITEMS_PER_PAGE + index + 1;
return (
<div
key={entry.player_id}
className={`table-row ${getRankClass(rank)}`}
onClick={() => navigate(`/profile/${entry.player_id}`)}
>
<div className="col-rank">
<span className="rank-badge">{getRankBadge(rank)}</span>
</div>
<div className="col-player">
<div className="player-name">{entry.name}</div>
<div className="player-username">@{entry.username}</div>
</div>
<div className="col-level">
<span className="level-badge">Lv {entry.level}</span>
</div>
<div className="col-value">
<span className="stat-value" style={{ color: selectedStat.color }}>
{formatStatValue(entry.value, selectedStat.key)}
</span>
</div>
</div>
);
})}
</div>
{Math.ceil(leaderboard.length / ITEMS_PER_PAGE) > 1 && (
<div className="pagination pagination-bottom">
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="pagination-btn"
>
</button>
<span className="pagination-info">
{currentPage} / {Math.ceil(leaderboard.length / ITEMS_PER_PAGE)}
</span>
<button
onClick={() => setCurrentPage(p => Math.min(Math.ceil(leaderboard.length / ITEMS_PER_PAGE), p + 1))}
disabled={currentPage >= Math.ceil(leaderboard.length / ITEMS_PER_PAGE)}
className="pagination-btn"
>
</button>
</div>
)}
</>
)}
</div>
</div>
</main>
</div>