659 lines
27 KiB
TypeScript
659 lines
27 KiB
TypeScript
"use client";
|
|
import Image from "next/image";
|
|
import { useState, useEffect, useRef } from "react";
|
|
import { usePrivy, useSolanaWallets } from "@privy-io/react-auth";
|
|
import { Connection, PublicKey } from "@solana/web3.js";
|
|
import { toast } from "sonner";
|
|
import "react-toastify/dist/ReactToastify.css";
|
|
import { CLUSTER_URL, API_URL, API_BASE_URL } from "@/data/shared";
|
|
import { useFundWallet } from "@privy-io/react-auth/solana";
|
|
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 } 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;
|
|
}
|
|
|
|
interface Opponent {
|
|
username: string;
|
|
x_profile_url: string;
|
|
}
|
|
|
|
export default function PrivyButton() {
|
|
const { login, logout, user, linkTwitter, unlinkTwitter } = usePrivy();
|
|
const { fundWallet } = useFundWallet();
|
|
const { wallets, ready,exportWallet } = useSolanaWallets();
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [solWallet, setSolWallet] = useState("");
|
|
const [solBalance, setSolBalance] = useState("--");
|
|
const [gamesHistory, setGamesHistory] = useState<GameHistory[]>([]);
|
|
const [opponentInfo, setOpponentInfo] = useState<{ [key: string]: Opponent }>({});
|
|
const [gameImages, setGameImages] = useState<{ [key: string]: string }>({});
|
|
const [loading, setLoading] = useState(false);
|
|
const [failedImages, setFailedImages] = useState<Set<string>>(new Set());
|
|
const defaultPFP = '/duelfiassets/PFP (1).png';
|
|
|
|
const [username, setUsername] = useState("Tester");
|
|
const [bio, setBio] = useState("");
|
|
const [avatar, setAvatar] = useState<string | null>(null);
|
|
|
|
const [isUsernameClaimModalOpen, setIsUsernameClaimModalOpen] = useState(false);
|
|
const [newUsername, setNewUsername] = useState("");
|
|
|
|
const modalRef = useRef<HTMLDivElement>(null);
|
|
const updateSolWallet = () => {
|
|
wallets.forEach((wallet) => {
|
|
if (wallet.type === "solana") {
|
|
setSolWallet(wallet.address);
|
|
}
|
|
});
|
|
}
|
|
const fetchSolBalance = async () => {
|
|
updateSolWallet();
|
|
|
|
if (solWallet !== "") {
|
|
try {
|
|
const connection = new Connection(CLUSTER_URL, "confirmed");
|
|
const publicKey = new PublicKey(solWallet);
|
|
const balance = await connection.getBalance(publicKey);
|
|
setSolBalance((balance / 1e9).toFixed(2));
|
|
} catch (error) {
|
|
console.error("Failed to get balance:", error);
|
|
setSolBalance("0");
|
|
}
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
const intervalId = setInterval(() => {
|
|
fetchSolBalance();
|
|
}, 15000); // 5000 milliseconds = 5 seconds
|
|
|
|
// Cleanup function to clear the interval on unmount or when `ready` changes
|
|
return () => clearInterval(intervalId);
|
|
}, [ready]);
|
|
|
|
const saveProfileChanges = async () => {
|
|
if (!user) {
|
|
toast.error("User not found!");
|
|
return;
|
|
}
|
|
|
|
const updateUrl = `${API_URL}update_profile.php?id=${user.id}&username=${username}&bio=${bio}`;
|
|
|
|
try {
|
|
const response = await fetch(updateUrl);
|
|
const data = await response.text();
|
|
|
|
if (data == "0") {
|
|
toast.success("Profile updated successfully!");
|
|
} else {
|
|
toast.error("Failed to update profile.");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error updating profile:", error);
|
|
toast.error("An error occurred while updating the profile.");
|
|
}
|
|
};
|
|
|
|
const fetchUserData = async () => {
|
|
if (user) {
|
|
const apiUrl = `${API_URL}get_user_by_id.php?id=${user?.id}`;
|
|
try {
|
|
const response = await fetch(apiUrl);
|
|
const data = await response.json();
|
|
if (data === 0) {
|
|
setIsUsernameClaimModalOpen(true); // Show modal if user isn't registered
|
|
} else {
|
|
setUsername(data.username || "Tester");
|
|
setBio(data.bio || "");
|
|
|
|
// Check if the user has a profile picture URL and update in database
|
|
const customProfileUrl = `${API_URL}profile_pics/${user.id}.jpg`;
|
|
const profilePictureUrl = user?.twitter?.profilePictureUrl ?? customProfileUrl;
|
|
if (profilePictureUrl) {
|
|
const updatePicUrlApi = `${API_URL}update_x_pic_url.php?id=${user?.id}&url=${profilePictureUrl}`;
|
|
await fetch(updatePicUrlApi);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to fetch user data:", error);
|
|
}
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (wallets) {
|
|
fetchSolBalance();
|
|
}
|
|
}, [user, wallets]);
|
|
|
|
useEffect(() => {
|
|
function handleClickOutside(event: MouseEvent) {
|
|
if (modalRef.current && !modalRef.current.contains(event.target as Node)) {
|
|
setIsModalOpen(false);
|
|
}
|
|
}
|
|
if (isModalOpen) {
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|
} else {
|
|
document.removeEventListener("mousedown", handleClickOutside);
|
|
}
|
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
}, [isModalOpen]);
|
|
|
|
// Fetch user data on login
|
|
useEffect(() => {
|
|
if (user) {
|
|
fetchUserData();
|
|
}
|
|
}, [user]);
|
|
|
|
const customLogin = () => {
|
|
login({ walletChainType: "solana-only" });
|
|
};
|
|
|
|
const copyToClipboard = async () => {
|
|
if (solWallet) {
|
|
await navigator.clipboard.writeText(solWallet);
|
|
toast.success("Wallet address copied!");
|
|
}
|
|
};
|
|
|
|
const handleAvatarChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.onloadend = async () => {
|
|
|
|
try {
|
|
// Ensure the user is authenticated and the privy_id is available
|
|
if (!user?.id) {
|
|
toast.error('No Privy ID found!');
|
|
return;
|
|
}
|
|
|
|
// Prepare the form data
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
formData.append('privy_id', user.id); // Append the privy_id
|
|
|
|
// Upload the avatar image to your server
|
|
const uploadResponse = await fetch(`${API_URL}upload_profile_picture.php`, {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
|
|
const uploadData = await uploadResponse.json();
|
|
console.log(uploadData);
|
|
if (uploadData.success) {
|
|
// Get the image URL from the response (assuming it returns the image path)
|
|
const imageUrl = uploadData.imageUrl;
|
|
|
|
// Update the avatar state and database
|
|
setAvatar(imageUrl);
|
|
const updatePicUrlApi = `${API_URL}update_x_pic_url.php?id=${user?.id}&url=${imageUrl}`;
|
|
await fetch(updatePicUrlApi);
|
|
|
|
toast.success('Profile picture uploaded successfully!');
|
|
} else {
|
|
|
|
toast.error('Failed to upload profile picture!');
|
|
}
|
|
} catch (error) {
|
|
console.error("Error uploading avatar:", error);
|
|
toast.error("An error occurred while uploading the profile picture.");
|
|
}
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
};
|
|
|
|
|
|
const handleUsernameClaim = async () => {
|
|
if (newUsername.trim()) {
|
|
const apiUrl = `${API_URL}register.php?id=${user?.id}&username=${newUsername}`;
|
|
try {
|
|
const response = await fetch(apiUrl);
|
|
const data = await response.text();
|
|
if (data == "0") {
|
|
toast.success("Username claimed successfully!");
|
|
setIsUsernameClaimModalOpen(false);
|
|
} else {
|
|
toast.error("Failed to claim username.");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error claiming username:", error);
|
|
toast.error("An error occurred while claiming your username.");
|
|
}
|
|
} else {
|
|
toast.error("Username cannot be empty!");
|
|
}
|
|
};
|
|
|
|
const twitterProfilePic: string = user?.twitter?.profilePictureUrl ?? "";
|
|
const customProfileUrl = `${API_BASE_URL}profile_pics/${user?.id}.jpg`;
|
|
|
|
useEffect(() => {
|
|
if (isModalOpen && user) {
|
|
const fetchGames = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await axios.get(
|
|
`${API_URL}get_game_history.php?uid=${user.id}`
|
|
);
|
|
const gameData = res.data || [];
|
|
setGamesHistory(gameData);
|
|
|
|
const opponentIds: string[] = gameData.map((game: GameHistory) =>
|
|
game.master_id === user.id ? 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();
|
|
}
|
|
}, [isModalOpen, user]);
|
|
|
|
const handleViewTxClick = (address: string) => {
|
|
window.open(`${EXPLORER_ADDRESS_TEMPLATE.replace("{address}",address)}`, "_blank");
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{user ? (
|
|
<>
|
|
<div className="flex items-center justify-between gap-4">
|
|
<button
|
|
onClick={() => {
|
|
setIsModalOpen(true);
|
|
fetchSolBalance();
|
|
fetchUserData();
|
|
}}
|
|
className="bg-[rgb(248,144,22)] hover:bg-[rgb(248,200,100)] text-black px-6 py-3 rounded-md transition duration-300 hover:scale-105"
|
|
>
|
|
<span className="font-bold">Connected</span>
|
|
<p className="text-xs font-mono text-gray-700">
|
|
{solWallet?.slice(0, 6)}...{solWallet?.slice(-4)}
|
|
</p>
|
|
</button>
|
|
|
|
<p className="text-s font-mono text-white">
|
|
<Image
|
|
src="/duelfiassets/solana logo.png"
|
|
alt="SOL"
|
|
width={25}
|
|
height={25}
|
|
className="inline mx-2"
|
|
/>
|
|
{solBalance}
|
|
</p>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<button
|
|
onClick={customLogin}
|
|
disabled={!ready}
|
|
className={`bg-[rgb(248,144,22)] hover:bg-[rgb(248,200,100)] text-black font-bold px-6 py-3 rounded-md transition duration-300 hover:scale-115 w-30 mx-3 ${!ready ? 'opacity-50 cursor-not-allowed' : ''}`}
|
|
>
|
|
{ready ? "Sign In" : "Loading..."}
|
|
</button>
|
|
)}
|
|
|
|
{isModalOpen && user && (
|
|
<div className="fixed inset-0 bg-black/70 flex justify-center items-start pt-10 z-50 overflow-y-auto">
|
|
<div
|
|
ref={modalRef}
|
|
className="bg-[rgb(30,30,30)] text-white w-full max-w-2xl p-6 rounded-2xl shadow-lg transform transition-transform duration-300 animate-slide-down space-y-6 mb-10"
|
|
>
|
|
<div className="flex justify-between items-center">
|
|
<h2 className="text-2xl font-bold mb-2">Your Profile</h2>
|
|
<button
|
|
onClick={() => setIsModalOpen(false)}
|
|
className="text-gray-400 hover:text-white transition duration-300 hover:scale-105"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Avatar + Link Twitter Row */}
|
|
<div className="flex items-center justify-between gap-4 flex-wrap">
|
|
<div className="flex items-center gap-4">
|
|
<Image
|
|
src={user?.twitter ? (twitterProfilePic) : (avatar ?? customProfileUrl)}
|
|
alt="Profile"
|
|
width={64}
|
|
height={64}
|
|
className="rounded-full border border-gray-500 object-cover"
|
|
/>
|
|
{(!user.twitter) ? (<label className="bg-gray-800 hover:bg-gray-700 text-white text-sm px-4 py-2 rounded-md cursor-pointer flex items-center gap-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
|
</svg>
|
|
Change
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={handleAvatarChange}
|
|
className="hidden"
|
|
/>
|
|
</label>) : (<></>)}
|
|
</div>
|
|
<div>
|
|
{user.twitter ? (
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm text-gray-300 font-mono">
|
|
Connected as {user.twitter.username}
|
|
</span>
|
|
<button
|
|
onClick={() => unlinkTwitter(user.twitter?.subject ?? "")}
|
|
className="bg-black text-white px-4 py-2 rounded-md text-sm hover:bg-gray-900 flex items-center gap-2"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
|
</svg>
|
|
Unlink
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<button
|
|
onClick={linkTwitter}
|
|
className="bg-black text-white px-4 py-2 rounded-md text-sm hover:bg-gray-900 flex items-center gap-2"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
</svg>
|
|
Link X (Twitter)
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Username */}
|
|
<div>
|
|
<label className="block text-xs text-gray-400 mb-1">Username</label>
|
|
<input
|
|
type="text"
|
|
value={username}
|
|
onChange={(e) => setUsername(e.target.value)}
|
|
className="w-full bg-gray-800 text-white p-2 rounded-md"
|
|
/>
|
|
</div>
|
|
|
|
{/* Bio */}
|
|
<div>
|
|
<label className="block text-xs text-gray-400 mb-1">Bio</label>
|
|
<textarea
|
|
value={bio}
|
|
onChange={(e) => setBio(e.target.value)}
|
|
className="w-full bg-gray-800 text-white p-2 rounded-md"
|
|
rows={2}
|
|
/>
|
|
</div>
|
|
|
|
{/* Save Button */}
|
|
<button
|
|
className="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-md font-semibold flex items-center gap-2"
|
|
onClick={saveProfileChanges}
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
Save Changes
|
|
</button>
|
|
|
|
{/* Divider */}
|
|
<hr className="border-gray-600" />
|
|
|
|
{/* Wallet Info */}
|
|
<div>
|
|
<p className="text-gray-400 text-xs font-mono mb-1">Connected Wallet</p>
|
|
<div className="flex items-center justify-between bg-gray-800 p-2 rounded-md">
|
|
<div className="flex items-center gap-2">
|
|
<p className="font-mono text-sm truncate">{solWallet}</p>
|
|
{wallets.some(wallet => wallet.type === "solana" && wallet.address === solWallet && wallet.connectorType === "embedded") && (
|
|
<span title="Embedded Wallet" className="text-gray-400 hover:text-white transition">
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fillRule="evenodd" d="M4 4a2 2 0 00-2 2v4a2 2 0 002 2V6h10a2 2 0 00-2-2H4zm2 6a2 2 0 012-2h8a2 2 0 012 2v4a2 2 0 01-2 2H8a2 2 0 01-2-2v-4zm6 4a2 2 0 100-4 2 2 0 000 4z" clipRule="evenodd" />
|
|
</svg>
|
|
</span>
|
|
)}
|
|
</div>
|
|
<button
|
|
onClick={copyToClipboard}
|
|
className="text-gray-300 hover:text-white transition p-1"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<p className="text-gray-400 text-xs font-mono mt-4">SOL Balance</p>
|
|
<p className="text-sm mb-3">{solBalance} SOL</p>
|
|
|
|
<div className="flex gap-2">
|
|
{wallets.some(wallet => wallet.type === "solana" && wallet.address === solWallet && wallet.connectorType === "embedded") && (
|
|
<button
|
|
onClick={() => {
|
|
exportWallet({ address: solWallet });
|
|
}}
|
|
className="flex-1 bg-[rgb(248,144,22)] hover:bg-[rgb(248,200,100)] text-black font-semibold py-2 rounded-xl transition hover:scale-105 flex items-center justify-center gap-2"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
Export Wallet
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={() => { fundWallet(solWallet, {}) }}
|
|
className={`${wallets.some(wallet => wallet.type === "solana" && wallet.address === solWallet && wallet.connectorType === "embedded") ? 'flex-1' : 'w-full'} bg-purple-600 hover:bg-purple-700 text-white font-semibold py-2 rounded-xl transition hover:scale-105 flex items-center justify-center gap-2`}
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
</svg>
|
|
Fund Wallet
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Divider */}
|
|
<hr className="border-gray-600" />
|
|
|
|
{/* Game History Section */}
|
|
<div>
|
|
<h3 className="text-xl font-bold mb-4">Game History</h3>
|
|
{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 max-h-96 overflow-y-auto">
|
|
{gamesHistory.map((game, idx) => {
|
|
const isUserMaster = game.master_id === user.id;
|
|
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"
|
|
>
|
|
<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={failedImages.has(profileUrl) ? defaultPFP : profileUrl}
|
|
alt="Profile"
|
|
width={40}
|
|
height={40}
|
|
className="w-10 h-10 rounded-full border border-gray-700 object-cover"
|
|
onError={(e) => {
|
|
// @ts-expect-error - Type mismatch expected
|
|
e.target.src = defaultPFP;
|
|
setFailedImages(prev => new Set(prev).add(profileUrl));
|
|
}}
|
|
/>
|
|
<Image
|
|
src={failedImages.has(gameImageUrl) ? "/duelfiassets/default-game-thumbnail.png" : gameImageUrl}
|
|
alt="Game Thumbnail"
|
|
width={64}
|
|
height={64}
|
|
className="w-16 h-16 rounded-md object-cover ml-4"
|
|
onError={(e) => {
|
|
// @ts-expect-error - Type mismatch expected
|
|
e.target.src = "/duelfiassets/default-game-thumbnail.png";
|
|
setFailedImages(prev => new Set(prev).add(gameImageUrl));
|
|
}}
|
|
/>
|
|
|
|
<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>
|
|
|
|
<div className="flex justify-end mt-6">
|
|
<button
|
|
className="w-[20%] bg-[rgb(248,22,22)] text-white font-semibold px-4 py-1.5 rounded-lg text-sm transition hover:bg-white hover:text-[rgb(248,22,22)] flex items-center justify-center gap-2"
|
|
onClick={() => {
|
|
logout();
|
|
setIsModalOpen(false);
|
|
}}
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
|
</svg>
|
|
Sign Out
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Username Claim Modal */}
|
|
{isUsernameClaimModalOpen && (
|
|
<div className="fixed inset-0 bg-black/70 flex justify-center items-center z-50">
|
|
<div className="bg-[rgb(30,30,30)] text-white w-full max-w-lg p-6 rounded-2xl shadow-lg space-y-6">
|
|
<h2 className="text-2xl font-bold">Claim Your Username</h2>
|
|
<input
|
|
type="text"
|
|
value={newUsername}
|
|
onChange={(e) => setNewUsername(e.target.value)}
|
|
className="w-full bg-[rgb(10,10,10)] text-white p-2 rounded-md"
|
|
placeholder="Enter your new username"
|
|
/>
|
|
<button
|
|
onClick={handleUsernameClaim}
|
|
className="w-full bg-[rgb(248,144,22)] hover:bg-orange-400 text-white px-4 py-2 rounded-md transition duration-500 hover:scale-105"
|
|
>
|
|
Claim
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|