mirror of
https://github.com/averel10/crypto_clash.git
synced 2026-03-12 10:58:11 +01:00
293 lines
10 KiB
TypeScript
293 lines
10 KiB
TypeScript
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>
|
||
);
|
||
}
|