243 lines
9.4 KiB
TypeScript
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>
|
|
);
|
|
}
|