mirror of
https://github.com/averel10/crypto_clash.git
synced 2026-03-12 10:58:11 +01:00
replace Lobby with GameList and update phase handling in Clash component
This commit is contained in:
293
crypto_clash_frontend/app/clash/GameList.tsx
Normal file
293
crypto_clash_frontend/app/clash/GameList.tsx
Normal file
@@ -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<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]);
|
||||||
|
|
||||||
|
// 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Web3 from "web3";
|
import Web3 from "web3";
|
||||||
import Lobby from "./Lobby";
|
import GameList from "./GameList";
|
||||||
import Commit from "./Commit";
|
import Commit from "./Commit";
|
||||||
import Reveal from "./Reveal";
|
import Reveal from "./Reveal";
|
||||||
|
|
||||||
@@ -14,7 +14,11 @@ export default function Clash() {
|
|||||||
const [status, setStatus] = useState<string>("");
|
const [status, setStatus] = useState<string>("");
|
||||||
|
|
||||||
// Inputs for contract functions
|
// 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
|
// Clear status when phase changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -68,7 +72,7 @@ export default function Clash() {
|
|||||||
Crypto Clash
|
Crypto Clash
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-center text-slate-600 dark:text-slate-300 mb-8">
|
<p className="text-center text-slate-600 dark:text-slate-300 mb-8">
|
||||||
{phase === "lobby" && "Register for a game to start."}
|
{phase === "games" && "Browse and join games."}
|
||||||
{phase === "commit" && "Commit your move."}
|
{phase === "commit" && "Commit your move."}
|
||||||
{phase === "reveal" && "Reveal your move."}
|
{phase === "reveal" && "Reveal your move."}
|
||||||
</p>
|
</p>
|
||||||
@@ -86,14 +90,14 @@ export default function Clash() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center mb-6 space-x-4">
|
<div className="flex justify-center mb-6 space-x-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => setPhase("lobby")}
|
onClick={() => setPhase("games")}
|
||||||
className={`px-4 py-2 rounded ${
|
className={`px-4 py-2 rounded ${
|
||||||
phase === "lobby"
|
phase === "games"
|
||||||
? "bg-blue-600 text-white"
|
? "bg-blue-600 text-white"
|
||||||
: "bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200"
|
: "bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Lobby
|
Games
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setPhase("commit")}
|
onClick={() => setPhase("commit")}
|
||||||
@@ -117,13 +121,14 @@ export default function Clash() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{phase === "lobby" && (
|
{phase === "games" && (
|
||||||
<Lobby
|
<GameList
|
||||||
account={account}
|
account={account}
|
||||||
contract={contract}
|
contract={contract}
|
||||||
config={config}
|
config={config}
|
||||||
web3={web3}
|
web3={web3}
|
||||||
setStatus={setStatus}
|
setStatus={setStatus}
|
||||||
|
onPlayClick={handlePlayClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{phase === "commit" && (
|
{phase === "commit" && (
|
||||||
@@ -148,7 +153,7 @@ export default function Clash() {
|
|||||||
{status && (
|
{status && (
|
||||||
<div
|
<div
|
||||||
className={`mt-6 p-4 rounded-lg ${
|
className={`mt-6 p-4 rounded-lg ${
|
||||||
status.includes("tx sent")
|
status.includes("✅") || status.includes("tx sent")
|
||||||
? "bg-green-50 dark:bg-green-900 text-green-800 dark:text-green-200"
|
? "bg-green-50 dark:bg-green-900 text-green-800 dark:text-green-200"
|
||||||
: "bg-red-50 dark:bg-red-900 text-red-800 dark:text-red-200"
|
: "bg-red-50 dark:bg-red-900 text-red-800 dark:text-red-200"
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
Reference in New Issue
Block a user