duelfi_admin/app/games/page.tsx
2025-06-08 22:04:00 +05:30

243 lines
9.4 KiB
TypeScript

'use client';
import { useState, useEffect, useMemo } from 'react';
import { Game, User, fetchGames, fetchUsers, truncateAddress, getDisplayGameName } from '../utils/api';
import Pagination from '../components/Pagination';
import SearchInput from '../components/SearchInput';
import Modal from '../components/Modal';
const ITEMS_PER_PAGE = 20;
export default function Games() {
const [games, setGames] = useState<Game[]>([]);
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [search, setSearch] = useState('');
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [showUserModal, setShowUserModal] = useState(false);
const handlePlayerClick = (e: React.MouseEvent, playerId: string) => {
e.preventDefault();
e.stopPropagation();
const user = users.find(u => u.active_wallet === playerId);
if (user) {
setSelectedUser(user);
setShowUserModal(true);
}
};
const getUserDisplayName = (userId: string): string => {
if (userId === 'na') return 'N/A';
const user = users.find(u => u.id === userId || u.active_wallet === userId);
if (user?.username) {
return user.username;
}
return truncateAddress(userId);
};
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
return date.toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
};
// Filter and paginate games
const filteredGames = useMemo(() => {
if (!search) return games;
const searchLower = search.toLowerCase();
return games.filter(game =>
game.ended_time.toLowerCase().includes(searchLower) ||
game.game.toLowerCase().includes(searchLower) ||
game.winner.toLowerCase().includes(searchLower) ||
game.master_id.toLowerCase().includes(searchLower) ||
game.client_id.toLowerCase().includes(searchLower) ||
game.master_score.includes(searchLower) ||
game.client_score.includes(searchLower) ||
game.wager.includes(searchLower)
);
}, [games, search]);
const currentGames = filteredGames.slice(
(currentPage - 1) * ITEMS_PER_PAGE,
currentPage * ITEMS_PER_PAGE
);
useEffect(() => {
const loadData = async () => {
try {
const [gamesData, usersData] = await Promise.all([
fetchGames(),
fetchUsers()
]);
setGames(gamesData);
setUsers(usersData);
} catch (err) {
setError('Failed to load data. Please try again later.');
console.error('Error loading data:', err);
} finally {
setLoading(false);
}
};
loadData();
}, []);
useEffect(() => {
setCurrentPage(1);
}, [search]);
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-lg text-[var(--text-primary)]">Loading...</div>
</div>
);
}
if (error) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-[var(--accent)]">{error}</div>
</div>
);
}
return (
<div className="space-y-6">
{/* User Details Modal */}
<Modal
isOpen={showUserModal}
onClose={() => {
setShowUserModal(false);
setSelectedUser(null);
}}
title="User Details"
>
{selectedUser && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Ref ID</p>
<p className="mt-1 text-[var(--text-primary)]">{selectedUser.ref_id}</p>
</div>
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Username</p>
<p className="mt-1 text-[var(--text-primary)]">{selectedUser.username || '-'}</p>
</div>
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Wallet Address</p>
<p className="mt-1 text-[var(--text-primary)]">{selectedUser.active_wallet || '-'}</p>
</div>
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">X Profile</p>
<p className="mt-1">
{selectedUser.x_profile_url ? (
<a
href={selectedUser.x_profile_url}
target="_blank"
rel="noopener noreferrer"
className="text-[var(--accent)] hover:text-[var(--accent-hover)]"
onClick={(e) => e.stopPropagation()}
>
View Profile
</a>
) : '-'}
</p>
</div>
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Referred By</p>
<p className="mt-1 text-[var(--text-primary)]">{selectedUser.referred_id === '-1' ? '-' : selectedUser.referred_id}</p>
</div>
</div>
</div>
)}
</Modal>
{/* Games History Section */}
<div className="bg-[var(--card-bg)] border border-[var(--card-border)] rounded-lg p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold text-[var(--text-primary)]">Game History</h2>
<div className="flex items-center space-x-4">
<SearchInput
placeholder="Search games..."
value={search}
onChange={setSearch}
/>
<div className="text-sm text-[var(--text-secondary)]">
Total Games: {filteredGames.length}
</div>
</div>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-[var(--card-border)]">
<thead className="bg-[var(--card-bg)]">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-secondary)] uppercase tracking-wider">Date</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-secondary)] uppercase tracking-wider">Game</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-secondary)] uppercase tracking-wider">Winner</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-secondary)] uppercase tracking-wider">Players</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-secondary)] uppercase tracking-wider">Score</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-secondary)] uppercase tracking-wider">Wager (SOL)</th>
</tr>
</thead>
<tbody className="divide-y divide-[var(--card-border)]">
{currentGames.map((game) => (
<tr key={game.address} className="hover:bg-[var(--card-bg)] transition-colors">
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
{formatDate(game.ended_time)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)] capitalize">
{getDisplayGameName(game.game)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)] capitalize">
{game.winner}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
<div>
Master: <button
onClick={(e) => handlePlayerClick(e, game.master_id)}
className="text-[var(--accent)] hover:text-[var(--accent-hover)] hover:underline cursor-pointer transition-colors"
>
{getUserDisplayName(game.master_id)}
</button>
</div>
<div>
Client: <button
onClick={(e) => handlePlayerClick(e, game.client_id)}
className="text-[var(--accent)] hover:text-[var(--accent-hover)] hover:underline cursor-pointer transition-colors"
disabled={game.client_id === 'na'}
>
{getUserDisplayName(game.client_id)}
</button>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
{game.master_score} - {game.client_score}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
{(parseInt(game.wager) / 100000000).toFixed(2)}
</td>
</tr>
))}
</tbody>
</table>
<Pagination
totalItems={filteredGames.length}
itemsPerPage={ITEMS_PER_PAGE}
currentPage={currentPage}
onPageChange={setCurrentPage}
/>
</div>
</div>
</div>
);
}