diff --git a/crypto_clash_frontend/app/clash/GameList.tsx b/crypto_clash_frontend/app/clash/GameList.tsx new file mode 100644 index 0000000..4620fd8 --- /dev/null +++ b/crypto_clash_frontend/app/clash/GameList.tsx @@ -0,0 +1,293 @@ +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) { + const [games, setGames] = useState([]); + const [loading, setLoading] = useState(false); + const [newGameBet, setNewGameBet] = useState("0.01"); + const [refreshInterval, setRefreshInterval] = useState(null); + const [userGameIds, setUserGameIds] = useState>(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(); + 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]); + + // 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 ( +
+ {/* Create New Game Section */} +
+

+ ➕ Create New Game +

+
+ setNewGameBet(e.target.value)} + className="flex-1 min-w-[200px]" + /> + +
+

+ 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. +

+
+ + {/* Active Games List */} +
+

+ 🎮 Available Games ({games.length}) +

+ + {games.length === 0 ? ( +
+

No active games available.

+

Create a new game to get started!

+
+ ) : ( +
+ {games.map((game) => ( +
+ {/* Game ID */} +
+

+ Game ID +

+

+ #{game.gameId} +

+
+ + {/* Players Info */} +
+

+ Players +

+
+

+ A: {formatAddress(game.playerA)} +

+

+ B: {game.playerB === "0x0000000000000000000000000000000000000000" + ? "Waiting..." + : formatAddress(game.playerB)} +

+
+
+ + {/* Bet Amount */} +
+

+ Bet +

+

+ {game.initialBet} ETH +

+
+ + {/* Join/Play Button */} +
+ {userGameIds.has(game.gameId) ? ( + + ) : ( + + )} +
+
+ ))} +
+ )} +
+ + {/* Refresh Info */} +
+

🔄 Games refresh automatically every 2 seconds

+
+
+ ); +} diff --git a/crypto_clash_frontend/app/clash/page.tsx b/crypto_clash_frontend/app/clash/page.tsx index 96c7c42..daad444 100644 --- a/crypto_clash_frontend/app/clash/page.tsx +++ b/crypto_clash_frontend/app/clash/page.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import Web3 from "web3"; -import Lobby from "./Lobby"; +import GameList from "./GameList"; import Commit from "./Commit"; import Reveal from "./Reveal"; @@ -14,7 +14,11 @@ export default function Clash() { const [status, setStatus] = useState(""); // Inputs for contract functions - const [phase, setPhase] = useState<"lobby" | "commit" | "reveal">("lobby"); + const [phase, setPhase] = useState<"games" | "commit" | "reveal">("games"); + + const handlePlayClick = (gameId: number) => { + setPhase("commit"); + }; // Clear status when phase changes useEffect(() => { @@ -68,7 +72,7 @@ export default function Clash() { Crypto Clash

- {phase === "lobby" && "Register for a game to start."} + {phase === "games" && "Browse and join games."} {phase === "commit" && "Commit your move."} {phase === "reveal" && "Reveal your move."}

@@ -86,14 +90,14 @@ export default function Clash() {
- {phase === "lobby" && ( - )} {phase === "commit" && ( @@ -148,7 +153,7 @@ export default function Clash() { {status && (