283 lines
11 KiB
TypeScript
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>
|
|
);
|
|
}
|