feat: add player merge functionality with automatic Elo recalculation

This commit is contained in:
2026-03-30 22:04:31 -07:00
parent 6001eda9aa
commit fa7a422ce3
2 changed files with 415 additions and 55 deletions
+152 -55
View File
@@ -22,6 +22,11 @@ export default function AdminPlayersPage() {
const [editingPlayer, setEditingPlayer] = useState<Player | null>(null)
const [newName, setNewName] = useState("")
const [isSaving, setIsSaving] = useState(false)
// Merge player state
const [mergeMode, setMergeMode] = useState(false)
const [selectedPlayers, setSelectedPlayers] = useState<number[]>([])
const [isMerging, setIsMerging] = useState(false)
useEffect(() => {
fetchPlayers()
@@ -76,6 +81,47 @@ export default function AdminPlayersPage() {
}
}
const togglePlayerSelection = (playerId: number) => {
setSelectedPlayers(prev =>
prev.includes(playerId)
? prev.filter(id => id !== playerId)
: [...prev, playerId]
)
}
const handleMerge = async () => {
if (selectedPlayers.length < 2) return
setIsMerging(true)
try {
const response = await fetch('/api/admin/players/merge', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
playerIds: selectedPlayers,
}),
})
const data = await response.json()
if (data.success) {
alert(`Successfully merged ${selectedPlayers.length} players! Elo ratings have been recalculated.`)
// Refresh the player list
fetchPlayers()
setSelectedPlayers([])
setMergeMode(false)
} else {
alert(`Error: ${data.error}`)
}
} catch (err: unknown) {
alert(`Error: ${err instanceof Error ? err.message : 'Unknown error occurred'}`)
} finally {
setIsMerging(false)
}
}
const playerCount = players.length
return (
@@ -92,8 +138,37 @@ export default function AdminPlayersPage() {
Manage {playerCount} player{playerCount !== 1 ? 's' : ''} in the system
</p>
</div>
<div className="flex space-x-2">
<button
onClick={() => setMergeMode(!mergeMode)}
className={`px-4 py-2 rounded-md shadow-sm text-sm font-medium ${
mergeMode
? 'bg-purple-600 text-white hover:bg-purple-700'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
{mergeMode ? 'Cancel Merge' : 'Merge Players'}
</button>
</div>
</div>
{mergeMode && selectedPlayers.length > 0 && (
<div className="mb-4 p-4 bg-purple-50 rounded-lg">
<div className="flex items-center justify-between">
<span className="text-purple-700">
{selectedPlayers.length} player{selectedPlayers.length !== 1 ? 's' : ''} selected for merging
</span>
<button
onClick={handleMerge}
disabled={isMerging || selectedPlayers.length < 2}
className="px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700 disabled:opacity-50"
>
{isMerging ? 'Merging...' : `Merge ${selectedPlayers.length} Players`}
</button>
</div>
</div>
)}
{error && (
<div className="mb-4 p-4 bg-red-100 text-red-700 rounded">
{error}
@@ -129,63 +204,85 @@ export default function AdminPlayersPage() {
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{players.map((player) => (
<tr key={player.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10 bg-gray-200 rounded-full flex items-center justify-center">
<span className="text-sm font-medium text-gray-600">
{player.name.charAt(0).toUpperCase()}
</span>
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">
{player.name}
{players.map((player) => {
const isSelected = selectedPlayers.includes(player.id)
return (
<tr
key={player.id}
className={`${isSelected ? 'bg-purple-50' : 'hover:bg-gray-50'} ${
mergeMode ? 'cursor-pointer' : ''
}`}
onClick={() => mergeMode && togglePlayerSelection(player.id)}
>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
{mergeMode && (
<input
type="checkbox"
checked={isSelected}
onChange={() => togglePlayerSelection(player.id)}
className="mr-3 h-4 w-4 text-purple-600 focus:ring-purple-500 border-gray-300 rounded"
/>
)}
<div className="flex-shrink-0 h-10 w-10 bg-gray-200 rounded-full flex items-center justify-center">
<span className="text-sm font-medium text-gray-600">
{player.name.charAt(0).toUpperCase()}
</span>
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">
{player.name}
</div>
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{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.wins}-{player.losses}
</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>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{player.user ? (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
{player.user.email}
</span>
) : (
<span className="text-gray-400 italic">No account</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<div className="flex space-x-2">
<Link
href={`/players/${player.id}/profile`}
className="text-green-600 hover:text-green-900"
>
View
</Link>
<button
onClick={() => handleEditClick(player)}
className="text-blue-600 hover:text-blue-900"
>
Edit
</button>
</div>
</td>
</tr>
))}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{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.wins}-{player.losses}
</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>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{player.user ? (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
{player.user.email}
</span>
) : (
<span className="text-gray-400 italic">No account</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<div className="flex space-x-2">
<Link
href={`/players/${player.id}/profile`}
className="text-green-600 hover:text-green-900"
>
View
</Link>
{!mergeMode && (
<button
onClick={(e) => {
e.stopPropagation()
handleEditClick(player)
}}
className="text-blue-600 hover:text-blue-900"
>
Edit
</button>
)}
</div>
</td>
</tr>
)
})}
</tbody>
</table>
</div>