206 lines
6.9 KiB
TypeScript
206 lines
6.9 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";
|
|
|
|
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);
|
|
|
|
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">
|
|
×
|
|
</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>
|
|
<img
|
|
src={profileUrl}
|
|
alt="Profile"
|
|
className="w-10 h-10 rounded-full border border-gray-700 object-cover"
|
|
/>
|
|
<img
|
|
src={gameImageUrl}
|
|
alt="Game Thumbnail"
|
|
className="w-16 h-16 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>
|
|
);
|
|
}
|