duelfi_web/src/components/OpenGames.tsx
2025-04-16 03:39:52 +00:00

283 lines
11 KiB
TypeScript

import { useEffect, useState } from "react";
import Image from "next/image";
import { games } from "../data/games";
import { usePrivy, useSolanaWallets } from "@privy-io/react-auth";
import { joinBet } from "@/shared/solana_helpers";
import { Bet } from "../types/Bet";
import { fetchUserById } from "@/shared/data_fetcher";
import { toast } from "sonner";
import { connection, EXPLORER_TX_TEMPLATE, API_BASE_URL } from "@/data/shared";
import { CONFIRMATION_THRESHOLD, WAGER_PRIZE_MULT } from "@/shared/constants";
interface GameModalProps {
bets: Bet[];
}
export default function YourGames({ bets }: GameModalProps) {
const { wallets } = useSolanaWallets();
const [myBets, setMyBets] = useState<Bet[]>([]);
const [loading, setLoading] = useState(true);
const { user } = usePrivy();
const [selectedBet, setSelectedBet] = useState<Bet | null>(null); // Track selected bet
const [isProcessing, setIsProcessing] = useState(false); // Track processing state
const [failedImages, setFailedImages] = useState<Set<string>>(new Set());
const defaultPFP = '/duelfiassets/PFP (1).png';
const handleJoinGame = async () => {
if (!selectedBet) return;
let wallet = wallets[0];
wallets.forEach((_wallet) => {
if (wallet.type === "solana") {
wallet = _wallet;
}
});
setIsProcessing(true);
toast.loading("Joining Bet");
try {
const tx = await joinBet(wallet, user?.id ?? "", selectedBet.id, selectedBet.address);
const url = EXPLORER_TX_TEMPLATE.replace("{address}", tx);
if (tx.length > 5) {
connection.confirmTransaction(tx, CONFIRMATION_THRESHOLD).finally(()=>{
toast.dismiss();
toast.success("Joined game successfully!", {
action: {
label: "View TX",
onClick: () => window.open(url, "_blank"),
},
});
})
} else {
toast.dismiss();
toast.error("Failed to join this game");
}
} catch (error) {
console.error("Error joining bet:", error);
toast.error("Failed to join this game");
} finally {
setIsProcessing(false);
setSelectedBet(null);
}
};
const updateBets = async () => {
let filteredBets = bets;
if(user){
let wallet = wallets[0];
wallets.forEach((_wallet) => {
if (wallet.type === "solana") {
wallet = _wallet;
}
});
filteredBets = bets.filter((bet) => bet.owner !== wallet.address);
}else{
console.log("No user found, showing all bets");
}
const enrichedBets = await Promise.all(
filteredBets.map(async (bet) => {
const ownerProfile = await fetchUserById(bet.owner_id);
return {
...bet,
ownerProfile,
};
})
);
setMyBets(enrichedBets);
setLoading(false);
console.log(`Got ${bets.length} bets, enriched to ${enrichedBets.length}`);
};
useEffect(() => {
updateBets();
const interval = setInterval(updateBets, 10000);
return () => clearInterval(interval);
}, [bets, updateBets]);
return (
<section className="py-16 px-6">
<h2 className="text-3xl font-bold text-white mb-6">Open Games</h2>
{loading ? (
<p className="text-gray-400">Loading Open games...</p>
) : myBets.length === 0 ? (
<p className="text-gray-400">No open games available</p>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{myBets.map((bet) => {
console.log(`Bet ${bet}`);
const game = games.find((g) => g.id === bet.id);
let ownerPFP = bet.ownerProfile?.x_profile_url || `${API_BASE_URL}profile_pics/${bet.ownerProfile?.id}.jpg`;
if (!game) return null;
// Check if this image has already failed to load
if (failedImages.has(ownerPFP)) {
ownerPFP = defaultPFP;
}
return (
<div
key={bet.address}
className="relative group bg-[rgb(30,30,30)] rounded-2xl p-2 w-50 overflow-hidden cursor-pointer transition-all duration-300"
onClick={() => setSelectedBet(bet)} // Open modal
>
<div className="relative w-full h-60 overflow-hidden rounded-xl">
<Image
src={game.thumbnail}
alt={game.name}
layout="fill"
objectFit="cover"
className="transition-all duration-300 group-hover:brightness-50"
/>
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<span className="text-white text-xl font-bold">Join</span>
</div>
</div>
<div className="mt-4 px-3 text-left">
<h3 className="text-lg font-semibold text-white py-2">{game.name}</h3>
<div className="flex justify-between text-xs font-mono py-1">
<p className="text-gray-400">Wager</p>
<p className="text-gray-400">Prize</p>
</div>
<div className="flex justify-between text-xs font-mono py-1">
<p className="text-white">{bet.wager} SOL</p>
<p className="text-white">{(bet.wager * 2 * WAGER_PRIZE_MULT).toFixed(3)} SOL</p>
</div>
{bet.ownerProfile && (
<div className="flex items-center gap-2 mt-4">
<Image
src={ownerPFP}
alt={bet.ownerProfile.username}
width={24}
height={24}
className="rounded-full"
onError={(e) => {
//@ts-expect-error e.target has no src, but it needs to be changed
e.target.src = defaultPFP;
setFailedImages(prev => new Set(prev).add(ownerPFP));
}}
/>
<p className="text-white text-sm font-mono">{bet.ownerProfile.username}</p>
</div>
)}
</div>
</div>
);
})}
</div>
)}
{/* Modal */}
{selectedBet && (
<div className="fixed inset-0 flex items-center justify-center z-50 backdrop-blur-sm bg-black/50">
<div className="bg-[rgb(30,30,30)] text-white p-6 rounded-lg shadow-lg max-w-sm w-full">
{isProcessing ? (
<div className="flex flex-col items-center">
<svg
className="animate-spin h-8 w-8 text-blue-400 mb-4"
viewBox="0 0 24 24"
fill="none"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8v8H4z"
></path>
</svg>
<h3 className="text-lg font-semibold">Processing...</h3>
<p className="text-gray-400 text-sm mt-2">Joining the Game, please wait...</p>
</div>
) : (
<>
<h2 className="text-xl font-bold mb-4">Are you sure to join this Game?</h2>
<div className="flex gap-4 mb-4">
<div className="w-1/2 relative h-48 overflow-hidden rounded-xl">
<Image
src={games.find(g => g.id === selectedBet.id)?.thumbnail || ''}
alt={games.find(g => g.id === selectedBet.id)?.name || ''}
layout="fill"
objectFit="cover"
/>
</div>
<div className="w-1/2 flex flex-col justify-center space-y-4">
{selectedBet.ownerProfile && (
<>
<p className="text-gray-400 mb-2">Offered by:</p>
<div className="flex items-center">
{(() => {
let modalOwnerPFP = selectedBet.ownerProfile.x_profile_url || `${API_BASE_URL}profile_pics/${selectedBet.ownerProfile.id}.jpg`;
if (failedImages.has(modalOwnerPFP)) {
modalOwnerPFP = defaultPFP;
}
return (
<Image
src={modalOwnerPFP}
alt={selectedBet.ownerProfile.username}
width={32}
height={32}
className="w-8 h-8 rounded-full mr-2"
onError={(e) => {
//@ts-expect-error e.target has no src, but it needs to be changed
e.target.src = defaultPFP;
setFailedImages(prev => new Set(prev).add(modalOwnerPFP));
}}
/>
);
})()}
<p className="font-bold">{selectedBet.ownerProfile.username}</p>
</div>
</>
)}
<div className="flex justify-between items-center">
<span className="text-gray-400">Entry:</span>
<span className="font-bold">{selectedBet.wager} SOL</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-400">Prize:</span>
<span className="font-bold">{(selectedBet.wager * 2 * WAGER_PRIZE_MULT).toFixed(3)} SOL</span>
</div>
</div>
</div>
<div className="flex justify-end gap-3 mt-4">
<button
onClick={() => setSelectedBet(null)}
className="bg-gray-700 px-4 py-2 rounded hover:scale-105 transition-all duration-300"
disabled={isProcessing}
>
Cancel
</button>
<button
onClick={handleJoinGame}
className="bg-[rgb(248,144,22)] text-black font-bold px-4 py-2 rounded hover:scale-105 transition-all duration-300"
disabled={isProcessing}
>
Confirm
</button>
</div>
</>
)}
</div>
</div>
)}
</section>
);
}