Files
crypto_clash/crypto_clash_frontend/app/clash/GameList.tsx

293 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from "react";
import Web3 from "web3";
import { Button } from "./Button";
import { Input } from "./Input";
interface GameListProps {
account: string;
contract: any;
config: Config | null;
web3: Web3 | null;
setStatus: (status: string) => void;
onPlayClick?: (gameId: number) => void;
}
interface GameInfo {
gameId: number;
playerA: string;
playerB: string;
initialBet: string;
isActive: boolean;
outcome: number;
}
export default function GameList({
account,
contract,
config,
web3,
setStatus,
onPlayClick,
}: Readonly<GameListProps>) {
const [games, setGames] = useState<GameInfo[]>([]);
const [loading, setLoading] = useState(false);
const [newGameBet, setNewGameBet] = useState<string>("0.01");
const [refreshInterval, setRefreshInterval] = useState<NodeJS.Timeout | null>(null);
const [userGameIds, setUserGameIds] = useState<Set<number>>(new Set());
// Fetch all active games
const fetchActiveGames = async () => {
if (!contract || !web3) return;
try {
const activeGameIds = await contract.methods.getActiveGameIds().call();
const gameDetails: GameInfo[] = [];
for (const gameId of activeGameIds) {
const details = await contract.methods.getGameDetails(gameId).call();
gameDetails.push({
gameId: Number(gameId),
playerA: details.playerAAddr,
playerB: details.playerBAddr,
initialBet: web3.utils.fromWei(details.initialBet, "ether"),
isActive: details.isActive,
outcome: Number(details.outcome),
});
}
setGames(gameDetails);
// Check which games the user is participating in
if (account) {
const userGames = new Set<number>();
for (const game of gameDetails) {
if (
game.playerA.toLowerCase() === account.toLowerCase() ||
game.playerB.toLowerCase() === account.toLowerCase()
) {
userGames.add(game.gameId);
}
}
setUserGameIds(userGames);
}
} catch (err: any) {
console.error("Failed to fetch games:", err.message);
}
};
// Auto-refresh games every 2 seconds
useEffect(() => {
fetchActiveGames();
const interval = setInterval(() => {
fetchActiveGames();
}, 2000);
setRefreshInterval(interval);
return () => {
if (interval) clearInterval(interval);
};
}, [contract, web3, account]);
// Join an existing game
const handleJoinGame = async (gameId: number, bet: string) => {
if (!contract || !web3 || !account) return;
setLoading(true);
setStatus("");
try {
const betWei = web3.utils.toWei(bet || "0.01", "ether");
const tx = contract.methods.register(gameId);
const gas = await tx.estimateGas({ from: account, value: betWei });
const result = await (globalThis as any).ethereum.request({
method: "eth_sendTransaction",
params: [
{
from: account,
to: config?.GAME_CONTRACT_ADDRESS,
data: tx.encodeABI(),
value: web3.utils.numberToHex(betWei),
gas: web3.utils.toHex(gas),
chainId: web3.utils.toHex(await web3.eth.net.getId()),
},
],
});
setStatus("✅ Joined game! Transaction: " + result);
await new Promise((resolve) => setTimeout(resolve, 2000));
fetchActiveGames();
} catch (err: any) {
setStatus("❌ Failed to join game: " + err.message);
console.error(err);
} finally {
setLoading(false);
}
};
// Create a new game
const handleCreateGame = async () => {
if (!contract || !web3 || !account) return;
setLoading(true);
setStatus("");
try {
const betWei = web3.utils.toWei(newGameBet || "0.01", "ether");
const tx = contract.methods.register(0); // 0 means create new game
const gas = await tx.estimateGas({ from: account, value: betWei });
const result = await (globalThis as any).ethereum.request({
method: "eth_sendTransaction",
params: [
{
from: account,
to: config?.GAME_CONTRACT_ADDRESS,
data: tx.encodeABI(),
value: web3.utils.numberToHex(betWei),
gas: web3.utils.toHex(gas),
chainId: web3.utils.toHex(await web3.eth.net.getId()),
},
],
});
setStatus("✅ Created new game! Transaction: " + result);
setNewGameBet("0.01");
await new Promise((resolve) => setTimeout(resolve, 2000));
fetchActiveGames();
} catch (err: any) {
setStatus("❌ Failed to create game: " + err.message);
console.error(err);
} finally {
setLoading(false);
}
};
const formatAddress = (addr: string) => {
if (!addr || addr === "0x0000000000000000000000000000000000000000") return "-";
return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
};
return (
<div className="space-y-6">
{/* Create New Game Section */}
<div className="border-2 border-green-300 dark:border-green-700 bg-green-50 dark:bg-green-900/20 p-4 rounded-lg">
<h3 className="font-semibold text-lg mb-3 text-slate-900 dark:text-white">
Create New Game
</h3>
<div className="flex gap-3 flex-wrap">
<Input
type="number"
min="0.01"
step="0.01"
placeholder="Bet in ETH (default 0.01)"
value={newGameBet}
onChange={(e) => setNewGameBet(e.target.value)}
className="flex-1 min-w-[200px]"
/>
<Button
onClick={handleCreateGame}
disabled={loading || !account || !contract}
variant="primary"
className="whitespace-nowrap"
>
Create Game
</Button>
</div>
<p className="text-xs text-slate-600 dark:text-slate-400 mt-2">
Enter the bet amount in ETH (e.g., 0.01 for 0.01 ETH). The first
player to join with the same or higher bet will play against you.
</p>
</div>
{/* Active Games List */}
<div className="border-2 border-blue-300 dark:border-blue-700 bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg">
<h3 className="font-semibold text-lg mb-4 text-slate-900 dark:text-white">
🎮 Available Games ({games.length})
</h3>
{games.length === 0 ? (
<div className="text-center py-8 text-slate-500 dark:text-slate-400">
<p className="text-sm">No active games available.</p>
<p className="text-xs mt-1">Create a new game to get started!</p>
</div>
) : (
<div className="space-y-2">
{games.map((game) => (
<div
key={game.gameId}
className="flex items-center gap-4 bg-white dark:bg-slate-700 p-4 rounded-lg shadow-sm hover:shadow-md transition-shadow border border-gray-200 dark:border-slate-600"
>
{/* Game ID */}
<div className="min-w-[80px]">
<p className="text-xs text-slate-500 dark:text-slate-400">
Game ID
</p>
<p className="font-semibold text-lg text-indigo-600 dark:text-indigo-400">
#{game.gameId}
</p>
</div>
{/* Players Info */}
<div className="flex-1 min-w-[200px]">
<p className="text-xs text-slate-500 dark:text-slate-400 mb-1">
Players
</p>
<div className="space-y-1">
<p className="font-mono text-sm text-slate-700 dark:text-slate-300">
<span className="text-xs text-slate-500">A:</span> {formatAddress(game.playerA)}
</p>
<p className="font-mono text-sm text-slate-700 dark:text-slate-300">
<span className="text-xs text-slate-500">B:</span> {game.playerB === "0x0000000000000000000000000000000000000000"
? "Waiting..."
: formatAddress(game.playerB)}
</p>
</div>
</div>
{/* Bet Amount */}
<div className="min-w-[100px]">
<p className="text-xs text-slate-500 dark:text-slate-400">
Bet
</p>
<p className="font-semibold text-slate-900 dark:text-white">
{game.initialBet} ETH
</p>
</div>
{/* Join/Play Button */}
<div className="flex gap-2">
{userGameIds.has(game.gameId) ? (
<Button
onClick={() => onPlayClick?.(game.gameId)}
variant="primary"
className="whitespace-nowrap bg-emerald-600 hover:bg-emerald-500 focus-visible:outline-emerald-600"
>
Play
</Button>
) : (
<Button
onClick={() =>
handleJoinGame(game.gameId, game.initialBet)
}
disabled={
loading ||
!account ||
!contract ||
game.playerB !==
"0x0000000000000000000000000000000000000000"
}
variant="primary"
className="whitespace-nowrap"
>
{game.playerB ===
"0x0000000000000000000000000000000000000000"
? "Join"
: "Full"}
</Button>
)}
</div>
</div>
))}
</div>
)}
</div>
{/* Refresh Info */}
<div className="text-center text-xs text-slate-500 dark:text-slate-400">
<p>🔄 Games refresh automatically every 2 seconds</p>
</div>
</div>
);
}