282 lines
10 KiB
TypeScript
282 lines
10 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useMemo } from 'react';
|
|
import { Game, User, fetchGames, fetchUsers, truncateAddress } from '../utils/api';
|
|
import Pagination from '../components/Pagination';
|
|
import SearchInput from '../components/SearchInput';
|
|
|
|
const ITEMS_PER_PAGE = 20;
|
|
|
|
export default function Dashboard() {
|
|
const [users, setUsers] = useState<User[]>([]);
|
|
const [games, setGames] = useState<Game[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [currentUserPage, setCurrentUserPage] = useState(1);
|
|
const [currentGamePage, setCurrentGamePage] = useState(1);
|
|
const [userSearch, setUserSearch] = useState('');
|
|
const [gameSearch, setGameSearch] = useState('');
|
|
|
|
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(() => {
|
|
// Reset to first page when search changes
|
|
setCurrentUserPage(1);
|
|
}, [userSearch]);
|
|
|
|
useEffect(() => {
|
|
// Reset to first page when search changes
|
|
setCurrentGamePage(1);
|
|
}, [gameSearch]);
|
|
|
|
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'
|
|
});
|
|
};
|
|
|
|
const getGameResult = (game: Game) => {
|
|
const masterScore = parseInt(game.master_score);
|
|
const clientScore = parseInt(game.client_score);
|
|
if (masterScore === clientScore) return 'Draw';
|
|
return game.winner === 'master' ? 'Master Won' : 'Client Won';
|
|
};
|
|
|
|
// Filter and paginate users
|
|
const filteredUsers = useMemo(() => {
|
|
if (!userSearch) return users;
|
|
const searchLower = userSearch.toLowerCase();
|
|
return users.filter(user =>
|
|
user.ref_id.toLowerCase().includes(searchLower) ||
|
|
(user.username || '').toLowerCase().includes(searchLower) ||
|
|
(user.active_wallet || '').toLowerCase().includes(searchLower) ||
|
|
(user.x_profile_url || '').toLowerCase().includes(searchLower) ||
|
|
(user.referred_id || '').toLowerCase().includes(searchLower)
|
|
);
|
|
}, [users, userSearch]);
|
|
|
|
// Filter and paginate games
|
|
const filteredGames = useMemo(() => {
|
|
if (!gameSearch) return games;
|
|
const searchLower = gameSearch.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, gameSearch]);
|
|
|
|
const currentUsers = filteredUsers.slice(
|
|
(currentUserPage - 1) * ITEMS_PER_PAGE,
|
|
currentUserPage * ITEMS_PER_PAGE
|
|
);
|
|
|
|
const currentGames = filteredGames.slice(
|
|
(currentGamePage - 1) * ITEMS_PER_PAGE,
|
|
currentGamePage * ITEMS_PER_PAGE
|
|
);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
<div className="text-lg">Loading...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
<div className="text-red-500">{error}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Users Section */}
|
|
<div className="bg-white shadow rounded-lg p-6">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h2 className="text-xl font-semibold text-gray-900">Users</h2>
|
|
<div className="flex items-center space-x-4">
|
|
<SearchInput
|
|
placeholder="Search users..."
|
|
value={userSearch}
|
|
onChange={setUserSearch}
|
|
/>
|
|
<div className="text-sm text-gray-600">
|
|
Total Users: {filteredUsers.length}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="overflow-x-auto">
|
|
<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">
|
|
Ref ID
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Username
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Wallet
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
X Profile
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Referred By
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{currentUsers.map((user) => (
|
|
<tr key={user.ref_id}>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
|
{user.ref_id}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{user.username || '-'}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{truncateAddress(user.active_wallet) || '-'}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{user.x_profile_url ? (
|
|
<a
|
|
href={user.x_profile_url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-blue-600 hover:text-blue-800"
|
|
>
|
|
View Profile
|
|
</a>
|
|
) : '-'}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{user.referred_id === '-1' ? '-' : user.referred_id}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
<Pagination
|
|
totalItems={filteredUsers.length}
|
|
itemsPerPage={ITEMS_PER_PAGE}
|
|
currentPage={currentUserPage}
|
|
onPageChange={setCurrentUserPage}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Games History Section */}
|
|
<div className="bg-white shadow rounded-lg p-6">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h2 className="text-xl font-semibold text-gray-900">Game History</h2>
|
|
<div className="flex items-center space-x-4">
|
|
<SearchInput
|
|
placeholder="Search games..."
|
|
value={gameSearch}
|
|
onChange={setGameSearch}
|
|
/>
|
|
<div className="text-sm text-gray-600">
|
|
Total Games: {filteredGames.length}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="overflow-x-auto">
|
|
<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">
|
|
Date
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Game
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Winner
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Players
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Score
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Result
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Wager (SOL)
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{currentGames.map((game) => (
|
|
<tr key={game.address}>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{formatDate(game.ended_time)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 capitalize">
|
|
{game.game}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 capitalize">
|
|
{game.winner}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
<div>Master: {truncateAddress(game.master_id)}</div>
|
|
<div>Client: {truncateAddress(game.client_id)}</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{game.master_score} - {game.client_score}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{getGameResult(game)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{(parseInt(game.wager) / 1000000).toFixed(2)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
<Pagination
|
|
totalItems={filteredGames.length}
|
|
itemsPerPage={ITEMS_PER_PAGE}
|
|
currentPage={currentGamePage}
|
|
onPageChange={setCurrentGamePage}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|