ce13bae949
Fixes #23 - Added overflow-x-hidden to body in root layout as defensive measure - Changed table containers from overflow-hidden to overflow-x-auto in admin/players page and RankingsClient (3 tables) - Added min-w-0 and overflow-hidden to Navigation flex containers to prevent links from pushing content off screen - Added flex-shrink-0 to EuchreCamp wordmark link
242 lines
10 KiB
TypeScript
242 lines
10 KiB
TypeScript
"use client"
|
|
|
|
import { useState } from "react"
|
|
import Link from "next/link"
|
|
|
|
// Define the Player type with all rating relationships
|
|
interface PlayerWithRatings {
|
|
id: number
|
|
name: string
|
|
currentElo: number
|
|
gamesPlayed: number
|
|
wins: number
|
|
losses: number
|
|
eloRating: { rating: number; gamesPlayed: number; wins: number; losses: number } | null
|
|
glicko2Rating: { rating: number; gamesPlayed: number; wins: number; losses: number } | null
|
|
openSkillRating: { rating: number; gamesPlayed: number; wins: number; losses: number } | null
|
|
}
|
|
|
|
export default function RankingsClient({ players }: { players: PlayerWithRatings[] }) {
|
|
const [activeTab, setActiveTab] = useState<"elo" | "openskill" | "glicko2">("elo")
|
|
|
|
return (
|
|
<>
|
|
{/* Tab Navigation */}
|
|
<div className="border-b border-gray-200 mb-6">
|
|
<nav className="-mb-px flex space-x-8">
|
|
<button
|
|
onClick={() => setActiveTab("elo")}
|
|
className={`${
|
|
activeTab === "elo"
|
|
? "border-green-500 text-green-600"
|
|
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
|
} whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm`}
|
|
>
|
|
Elo Rating
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab("openskill")}
|
|
className={`${
|
|
activeTab === "openskill"
|
|
? "border-green-500 text-green-600"
|
|
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
|
} whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm`}
|
|
>
|
|
OpenSkill
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab("glicko2")}
|
|
className={`${
|
|
activeTab === "glicko2"
|
|
? "border-green-500 text-green-600"
|
|
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
|
} whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm`}
|
|
>
|
|
Glicko2
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
|
|
{/* Elo Rating Tab */}
|
|
{activeTab === "elo" && (
|
|
<div className="bg-white shadow overflow-x-auto sm:rounded-lg">
|
|
<h2 className="text-xl font-semibold text-gray-900 px-6 py-4 border-b">
|
|
Elo Rating Rankings
|
|
</h2>
|
|
<table className="min-w-full divide-y divide-gray-200">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Rank
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Player
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Elo Rating
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Games Played
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Win Rate
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{players.map((player, index) => (
|
|
<tr key={player.id} className="hover:bg-gray-50">
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
|
{index + 1}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<Link
|
|
href={`/players/${player.id}/profile`}
|
|
className="text-green-600 hover:text-green-900"
|
|
>
|
|
{player.name}
|
|
</Link>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{player.eloRating?.rating ?? player.currentElo}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{player.gamesPlayed}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{player.gamesPlayed > 0
|
|
? `${((player.wins / player.gamesPlayed) * 100).toFixed(1)}%`
|
|
: "N/A"}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
|
|
{/* OpenSkill Rating Tab */}
|
|
{activeTab === "openskill" && (
|
|
<div className="bg-white shadow overflow-x-auto sm:rounded-lg">
|
|
<h2 className="text-xl font-semibold text-gray-900 px-6 py-4 border-b">
|
|
OpenSkill Rating Rankings
|
|
</h2>
|
|
<table className="min-w-full divide-y divide-gray-200">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Rank
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Player
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
OpenSkill Rating
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Games Played
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Win Rate
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{players
|
|
.filter(p => p.openSkillRating)
|
|
.sort((a, b) => (b.openSkillRating?.rating ?? 0) - (a.openSkillRating?.rating ?? 0))
|
|
.map((player, index) => (
|
|
<tr key={player.id} className="hover:bg-gray-50">
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
|
{index + 1}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<Link
|
|
href={`/players/${player.id}/profile`}
|
|
className="text-green-600 hover:text-green-900"
|
|
>
|
|
{player.name}
|
|
</Link>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{player.openSkillRating?.rating?.toFixed(1) ?? 'N/A'}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{player.openSkillRating?.gamesPlayed ?? 0}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{player.openSkillRating && player.openSkillRating.gamesPlayed > 0
|
|
? `${((player.openSkillRating.wins / player.openSkillRating.gamesPlayed) * 100).toFixed(1)}%`
|
|
: "N/A"}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
|
|
{/* Glicko2 Rating Tab */}
|
|
{activeTab === "glicko2" && (
|
|
<div className="bg-white shadow overflow-x-auto sm:rounded-lg">
|
|
<h2 className="text-xl font-semibold text-gray-900 px-6 py-4 border-b">
|
|
Glicko2 Rating Rankings
|
|
</h2>
|
|
<table className="min-w-full divide-y divide-gray-200">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Rank
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Player
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Glicko2 Rating
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Games Played
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Win Rate
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{players
|
|
.filter(p => p.glicko2Rating)
|
|
.sort((a, b) => (b.glicko2Rating?.rating ?? 0) - (a.glicko2Rating?.rating ?? 0))
|
|
.map((player, index) => (
|
|
<tr key={player.id} className="hover:bg-gray-50">
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
|
{index + 1}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<Link
|
|
href={`/players/${player.id}/profile`}
|
|
className="text-green-600 hover:text-green-900"
|
|
>
|
|
{player.name}
|
|
</Link>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{player.glicko2Rating?.rating?.toFixed(1) ?? 'N/A'}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{player.glicko2Rating?.gamesPlayed ?? 0}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{player.glicko2Rating && player.glicko2Rating.gamesPlayed > 0
|
|
? `${((player.glicko2Rating.wins / player.glicko2Rating.gamesPlayed) * 100).toFixed(1)}%`
|
|
: "N/A"}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</>
|
|
)
|
|
}
|