duelfi_web/src/components/GameHistory.tsx
2025-04-18 08:39:18 +05:30

211 lines
7.1 KiB
TypeScript

import { MouseEventHandler, useEffect, useState } from "react";
import axios from "axios";
import { Game } from "@/types/Game";
import { games } from "@/data/games";
import { WAGER_PRIZE_MULT } from "@/shared/constants";
import { EXPLORER_ADDRESS_TEMPLATE, API_URL } from "@/data/shared";
import Image from 'next/image';
interface GameHistory {
address: string;
master_score: string;
client_score: string;
winner: string;
wager: string;
master_id: string;
client_id: string;
game: string; // game ID
}
interface Opponent {
username: string;
x_profile_url: string;
}
interface GameHistoryModalProps {
userId: string | undefined;
isOpen: boolean;
onClose: MouseEventHandler;
}
export default function GameHistoryModal({
userId,
isOpen,
onClose,
}: GameHistoryModalProps) {
const [gamesHistory, setGamesHistory] = useState<GameHistory[]>([]);
const [opponentInfo, setOpponentInfo] = useState<{
[key: string]: Opponent;
}>({});
const [gameImages, setGameImages] = useState<{ [key: string]: string }>({});
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!isOpen || !userId) return;
const fetchGames = async () => {
setLoading(true);
try {
const res = await axios.get(
`${API_URL}get_game_history.php?uid=${userId}`
);
const gameData = res.data || [];
setGamesHistory(gameData);
console.log(`history data ${gameData}`);
const opponentIds: string[] = gameData.map((game: GameHistory) =>
game.master_id === userId ? game.client_id : game.master_id
);
const uniqueOpponentIds: string[] = Array.from(new Set(opponentIds));
const fetchedOpponentInfo: { [key: string]: Opponent } = {};
await Promise.all(
uniqueOpponentIds.map(async (uid) => {
try {
const response = await axios.get(
`${API_URL}get_user_by_id.php?id=${uid}`
);
const { username, x_profile_url } = response.data;
fetchedOpponentInfo[uid] = {
username,
x_profile_url,
};
} catch (err) {
console.error("Failed to fetch opponent info for", uid, err);
}
})
);
setOpponentInfo(fetchedOpponentInfo);
const gameDataWithImages: { [key: string]: string } = {};
await Promise.all(
gameData.map(async (gameHistory: GameHistory) => {
try {
const gameImage = games.find((game: Game) => game.id == gameHistory.game);
gameDataWithImages[gameHistory.game] = gameImage?.thumbnail ?? "";
} catch (err) {
console.error("Failed to fetch game image for", gameHistory.game, err);
}
})
);
setGameImages(gameDataWithImages);
} catch (err) {
console.error("Error fetching game history", err);
} finally {
setLoading(false);
}
};
fetchGames();
}, [isOpen, userId]);
const handleViewTxClick = (address: string) => {
// Open the transaction in a new tab (you can modify this URL to dynamically handle the TX)
window.open(`${EXPLORER_ADDRESS_TEMPLATE.replace("{address}",address)}`, "_blank");
};
if (!isOpen) return null;
return (
<div className="fixed top-0 right-0 h-full w-[400px] bg-[rgb(10,10,10)] shadow-lg z-50 p-6 overflow-y-auto">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold text-white">Game History</h2>
<button onClick={onClose} className="text-red-500 font-bold text-lg">
&times;
</button>
</div>
{loading ? (
<p className="text-gray-500">Loading...</p>
) : gamesHistory.length === 0 ? (
<p className="text-gray-500">No games played yet.</p>
) : (
<div className="space-y-4">
{gamesHistory.map((game, idx) => {
const isUserMaster = game.master_id === userId;
const userScore = isUserMaster
? game.master_score
: game.client_score;
const opponentScore = isUserMaster
? game.client_score
: game.master_score;
const opponentId = isUserMaster
? game.client_id
: game.master_id;
const didUserWin =
(isUserMaster && game.winner === "master") ||
(!isUserMaster && game.winner === "client");
const opponent = opponentInfo[opponentId];
const profileUrl =
opponent?.x_profile_url ||
(opponent?.username
? `${API_URL}profile_picture/${opponent.username}.jpg`
: "/duelfiassets/default-avatar.png");
const gameImageUrl = gameImages[game.game] || "/duelfiassets/default-game-thumbnail.png";
const wagerAmount = parseFloat(game.wager);
const outcomeText = didUserWin
? `+${(wagerAmount / 1e8) * 2 * WAGER_PRIZE_MULT} SOL`
: `-${(wagerAmount / 1e8)} SOL`;
return (
<div
key={idx}
className="relative border border-[rgb(30,30,30)] rounded-xl p-3 flex gap-3 items-center group"
>
{/* Card content */}
<div className="flex-1">
<p className="text-sm font-semibold text-white">
{opponent?.username || "Unknown Opponent"}
</p>
<p className="text-xs text-gray-400">
Score: {userScore} - {opponentScore}
</p>
<p
className={`text-sm font-semibold ${
didUserWin ? "text-green-500" : "text-gray-500"
}`}
>
{didUserWin ? "You won" : "You lost"}
</p>
<p className={`text-xs ${didUserWin ? "text-green-500" : "text-red-500"}`}>
{outcomeText}
</p>
</div>
<Image
src={profileUrl}
alt="Profile"
width={40}
height={40}
className="rounded-full border border-gray-700 object-cover"
/>
<Image
src={gameImageUrl}
alt="Game Thumbnail"
width={64}
height={64}
className="rounded-md object-cover ml-4"
/>
{/* View TX Action */}
<div className="absolute top-0 right-0 h-full w-28 bg-blue-500 text-white flex items-center justify-center opacity-0 group-hover:opacity-100 group-hover:translate-x-0 transition-all">
<button
onClick={() => handleViewTxClick(game.address)}
className="px-4 py-2 text-sm font-semibold"
>
View TX
</button>
</div>
</div>
);
})}
</div>
)}
</div>
);
}