profile data

This commit is contained in:
Sewmina 2025-04-04 13:56:44 +05:30
parent 145b7946f8
commit dd303efd01
14 changed files with 1173 additions and 758 deletions

View File

@ -2,6 +2,9 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
images: {
domains: ["pbs.twimg.com","vps.playpoolstudios.com"], // ✅ add Twitter's image domain
},
};
export default nextConfig;

View File

@ -52,3 +52,45 @@ body {
background-size: 50px 50px;
animation: scrollGrid 2s linear infinite;
}
/* Slide in from the top */
@keyframes slide-down {
0% {
transform: translateY(-100%);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
/* Slide out to the top */
@keyframes slide-up {
0% {
transform: translateY(0);
opacity: 1;
}
100% {
transform: translateY(-100%);
opacity: 0;
}
}
@keyframes slide-down {
0% {
transform: translateY(-100%);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
.modal-enter {
animation: slide-down 0.2s ease-out;
}
.modal-exit {
animation: slide-up 0.2s ease-in;
}

View File

@ -8,33 +8,36 @@ import { toSolanaWalletConnectors } from "@privy-io/react-auth/solana";
import { Toaster } from "sonner";
export default function Home() {
return (
<div className="bg-[rgb(22,22,22)]">
<PrivyProvider
appId="cm8spd7l600lfe4am1phq9qq8"
clientId="client-WY5i4HS6T7JP44iKMQUyZXwftzwKLFvEsGvMtFY1znXSj"
config={{
// Customize Privy's appearance in your app
appearance: {
theme: 'dark',
accentColor: '#f89016',
logo: 'https://your-logo-url'
},
// Create embedded wallets for users who don't have a wallet
embeddedWallets: {
createOnLogin: 'users-without-wallets'
},
externalWallets:{
solana:{connectors:toSolanaWalletConnectors()}
}
appId="cm8spd7l600lfe4am1phq9qq8"
clientId="client-WY5i4HS6T7JP44iKMQUyZXwftzwKLFvEsGvMtFY1znXSj"
config={{
// Customize Privy's appearance in your app
appearance: {
theme: 'dark',
accentColor: '#f89016',
logo: 'https://i.postimg.cc/3xymQbkZ/Logo-no-BG-4.png'
},
// Create embedded wallets for users who don't have a wallet
embeddedWallets: {
createOnLogin: 'users-without-wallets'
},
externalWallets: {
solana: { connectors: toSolanaWalletConnectors() }
}
}}
>
<Toaster position="top-right" richColors />
<Header/>
<HeroSection/>
<Footer/>
}}
>
<>
<Toaster position="top-right" richColors />
<Header />
<HeroSection />
<Footer />
</>
</PrivyProvider>
</div>

View File

@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useEffect, useState } from "react";
import { usePrivy, useSolanaWallets } from "@privy-io/react-auth";
import { toast } from "sonner";
import { games } from "../data/games";
@ -8,7 +8,7 @@ import { PriceSelection } from "./PriceSelection";
import { GameSelection } from "./GameSelection";
import { createBet } from "@/shared/solana_helpers";
import { Game } from "@/types/Game";
import { EXPLORER_TX_TEMPLATE } from "@/data/shared";
import { EXPLORER_TX_TEMPLATE } from "@/data/shared";
interface GameModalProps {
isOpen: boolean;
@ -17,19 +17,39 @@ interface GameModalProps {
export default function GameModal({ isOpen, onClose }: GameModalProps) {
const { wallets } = useSolanaWallets();
const { authenticated } = usePrivy();
const { authenticated, user } = usePrivy();
const [selectedGame, setSelectedGame] = useState<Game | null>(null);
const [selectedPrice, setSelectedPrice] = useState<number | null>(null);
const [isProcessing, setIsProcessing] = useState(false);
const [shouldRender, setShouldRender] = useState(isOpen);
const [animationClass, setAnimationClass] = useState("");
useEffect(() => {
if (isOpen) {
setShouldRender(true);
setAnimationClass("modal-enter");
} else {
setAnimationClass("modal-exit");
const timer = setTimeout(() => setShouldRender(false), 200);
return () => clearTimeout(timer);
}
}, [isOpen]);
const handleCreateGame = async () => {
if (!authenticated) {
toast.error("Please log in with Privy.");
return;
}
const wallet = wallets[0];
let wallet = wallets[0];
wallets.forEach((_wallet) => {
if (_wallet.type === "solana") {
wallet = _wallet;
}
});
if (!wallet) {
toast.error("Please connect your wallet.");
return;
@ -42,15 +62,16 @@ export default function GameModal({ isOpen, onClose }: GameModalProps) {
setIsProcessing(true);
try {
const tx = await createBet(wallet, selectedPrice, selectedGame);
const tx = await createBet(wallet, user?.id ?? "", selectedPrice, selectedGame);
const url = EXPLORER_TX_TEMPLATE.replace("{address}", tx);
toast.success(`Bet created successfully!`, {
action: {
label: "View TX",
onClick: () => window.open(url, "_blank"),
},
});
if (tx.length > 5) {
toast.success(`Bet created successfully!`, {
action: {
label: "View TX",
onClick: () => window.open(url, "_blank"),
},
});
}
onClose();
} catch (error) {
console.error("Error creating bet:", error);
@ -58,20 +79,39 @@ export default function GameModal({ isOpen, onClose }: GameModalProps) {
} finally {
setIsProcessing(false);
}
};
if (!isOpen) return null;
if (!shouldRender) return null;
return (
<div className="fixed inset-0 bg-black/70 flex justify-center items-start pt-10 z-50" onClick={onClose}>
<div className="bg-[rgb(30,30,30)] text-white w-full max-w-lg p-6 rounded-2xl shadow-lg" onClick={(e) => e.stopPropagation()}>
<div
className="fixed inset-0 bg-black/70 flex justify-center items-start pt-10 z-50"
onClick={onClose}
>
<div
className={`bg-[rgb(30,30,30)] text-white w-full max-w-lg p-6 rounded-2xl shadow-lg ${animationClass}`}
onClick={(e) => e.stopPropagation()}
>
{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
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">Creating your bet, please wait...</p>
@ -79,9 +119,15 @@ export default function GameModal({ isOpen, onClose }: GameModalProps) {
) : (
<>
<h2 className="text-xl font-bold mb-4">Create Game</h2>
<GameSelection games={games} selectedGame={selectedGame} onSelect={setSelectedGame} />
<PriceSelection selectedPrice={selectedPrice} onSelect={setSelectedPrice} />
<GameSelection
games={games}
selectedGame={selectedGame}
onSelect={setSelectedGame}
/>
<PriceSelection
selectedPrice={selectedPrice}
onSelect={setSelectedPrice}
/>
<button
className="mt-6 w-full py-2 rounded-xl font-semibold bg-[rgb(248,144,22)] text-black hover:bg-white hover:scale-105"
onClick={handleCreateGame}

View File

@ -78,8 +78,7 @@ export default function Header() {
</div>
{/* Mobile-only view (Logo & Login) */}
<div className="flex md:hidden items-center gap-4">
{/* Hamburger Button */}
<div className="flex md:hidden items-center ">
<PrivyButton></PrivyButton>
<button

View File

@ -1,41 +1,62 @@
import { useEffect, useState } from "react";
interface HowItWorksModalProps {
isOpen: boolean;
onClose: () => void;
}
isOpen: boolean;
onClose: () => void;
}
export function HowItWorksModal({ isOpen, onClose }: HowItWorksModalProps) {
if (!isOpen) return null;
export function HowItWorksModal({ isOpen, onClose }: HowItWorksModalProps) {
const [shouldRender, setShouldRender] = useState(isOpen);
const [animationClass, setAnimationClass] = useState("");
return (
<div className="fixed inset-0 bg-black/70 flex justify-center items-start pt-10 z-50" onClick={onClose}>
<div
className="bg-[rgb(30,30,30)] text-white w-full max-w-lg p-6 rounded-2xl shadow-lg transform transition-transform duration-300 animate-slide-down"
onClick={(e) => e.stopPropagation()}
>
<h2 className="text-xl font-bold mb-4">How It Works</h2>
useEffect(() => {
if (isOpen) {
setShouldRender(true);
setAnimationClass("modal-enter");
} else {
setAnimationClass("modal-exit");
// Wait for animation to finish before unmounting
const timer = setTimeout(() => setShouldRender(false), 200); // match animation duration
return () => clearTimeout(timer);
}
}, [isOpen]);
<div className="space-y-4">
{[
{ step: "Connect Your Wallet", desc: "Start by linking your wallet securely." },
{ step: "Create or Join Game", desc: "Pick a game and set a wager, or join an existing match." },
{ step: "Place Your Bet", desc: "Confirm your wager and get ready to play." },
{ step: "Claim Your Winnings", desc: "Win the game and collect your rewards instantly!" },
].map(({ step, desc }, index) => (
<div key={index}>
<h3 className="text-[rgb(248,144,22)] font-bold text-lg">{index + 1}. {step}</h3>
<p className="text-xs text-gray-400 font-mono">{desc}</p>
</div>
))}
</div>
if (!shouldRender) return null;
<button
className="mt-6 w-full bg-[rgb(248,144,22)] text-black font-semibold py-2 rounded-xl transition hover:bg-white hover:scale-105"
onClick={onClose}
>
Okay
</button>
return (
<div
className="fixed inset-0 bg-black/70 flex justify-center items-start pt-10 z-50"
onClick={onClose}
>
<div
className={`bg-[rgb(30,30,30)] text-white w-full max-w-lg p-6 rounded-2xl shadow-lg transform transition-transform duration-300 ${animationClass}`}
onClick={(e) => e.stopPropagation()}
>
<h2 className="text-xl font-bold mb-4">How It Works</h2>
<div className="space-y-4">
{[
{ step: "Connect Your Wallet", desc: "Start by linking your wallet securely." },
{ step: "Create or Join Game", desc: "Pick a game and set a wager, or join an existing match." },
{ step: "Place Your Bet", desc: "Confirm your wager and get ready to play." },
{ step: "Claim Your Winnings", desc: "Win the game and collect your rewards instantly!" },
].map(({ step, desc }, index) => (
<div key={index}>
<h3 className="text-[rgb(248,144,22)] font-bold text-lg">
{index + 1}. {step}
</h3>
<p className="text-xs text-gray-400 font-mono">{desc}</p>
</div>
))}
</div>
</div>
);
}
<button
className="mt-6 w-full bg-[rgb(248,144,22)] text-black font-semibold py-2 rounded-xl transition hover:bg-white hover:scale-105"
onClick={onClose}
>
Okay
</button>
</div>
</div>
);
}

View File

@ -4,16 +4,33 @@ import Image from "next/image";
import { games } from "../data/games";
import { useSolanaWallets } from "@privy-io/react-auth";
import { fetchOpenBets } from "@/shared/solana_helpers";
import {Bet} from "../types/Bet";
import { Bet } from "../types/Bet";
import { fetchUserById } from "@/shared/data_fetcher";
export default function YourGames() {
const { wallets, ready } = useSolanaWallets();
const [myBets, setMyBets] = useState<Bet[]>([]);
const [loading, setLoading] = useState(true);
// Function to fetch open bets
const updateBets= async ()=>{
const bets:Bet[] = await fetchOpenBets(wallets[0]);
setMyBets(bets.filter((bet) => bet.owner !== wallets[0].address));
const updateBets = async () => {
let wallet = wallets[0];
wallets.forEach((_wallet) => {
if (wallet.type === "solana") {
wallet = _wallet;
}
});
const bets: Bet[] = await fetchOpenBets(wallet);
const filteredBets = (bets.filter((bet) => bet.owner !== wallet.address));
const enrichedBets = await Promise.all(
filteredBets.map(async (bet) => {
const ownerProfile = await fetchUserById(bet.owner_id);
return {
...bet,
ownerProfile, // contains {username, x_profile_url, etc.}
};
})
);
setMyBets(enrichedBets);
setLoading(false);
console.log(`Got ${bets.length} bets`);
}
@ -33,41 +50,68 @@ export default function YourGames() {
{loading ? (
<p className="text-gray-400">Loading Open games...</p>
) :
myBets.length === 0 ? <></> :(
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{myBets.map((bet) => {
console.log(`Finding game for the id ${bet.id}`)
const game = games.find((g) => g.id === bet.id); // Match game
myBets.length === 0 ? <></> : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{myBets.map((bet) => {
console.log(`Finding game for the id ${bet.id}`)
const game = games.find((g) => g.id === bet.id); // Match game
if (!game) return null; // Skip unmatched bets
if (!game) return null; // Skip unmatched bets
return (
<div
key={bet.id + bet.owner}
className="relative group bg-[rgb(30,30,30)] rounded-2xl p-2 w-50 overflow-hidden cursor-pointer transition-all duration-300"
>
{/* Game Thumbnail */}
<div className="relative w-full h-40 overflow-hidden rounded-xl">
<Image
src={game.thumbnail}
alt={game.name}
layout="fill"
objectFit="cover"
className="transition-all duration-300 group-hover:brightness-50"
/>
return (
<div
key={bet.id + bet.owner}
className="relative group bg-[rgb(30,30,30)] rounded-2xl p-2 w-50 overflow-hidden cursor-pointer transition-all duration-300"
>
{/* Game Thumbnail */}
<div className="relative w-full h-40 overflow-hidden rounded-xl">
<Image
src={game.thumbnail}
alt={game.name}
layout="fill"
objectFit="cover"
className="transition-transform duration-300 group-hover:scale-110"
/>
{/* Join Overlay */}
<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>
{/* Game Info */}
<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).toFixed(2)} SOL</p>
</div>
{/* User Info */}
{bet.ownerProfile && (
<div className="flex items-center gap-2 mt-4">
<Image
src={bet.ownerProfile.x_profile_url || `https://vps.playpoolstudios.com/duelfi/profile_pics/${bet.ownerProfile.id}.jpg`}
alt={bet.ownerProfile.username}
width={24}
height={24}
className="rounded-full"
/>
<p className="text-white text-sm font-mono">{bet.ownerProfile.username}</p>
</div>
)}
</div>
</div>
{/* Game Info */}
<div className="mt-4 px-3 text-left">
<h3 className="text-lg font-semibold text-white py-2">{game.name}</h3>
<p className="text-xs text-gray-400 font-mono py-1">Wager</p>
<p className="text-xs text-white font-mono py-1">{bet.wager} SOL</p>
</div>
</div>
);
})}
</div>
)}
);
})}
</div>
)}
</section>
);
}

View File

@ -2,53 +2,106 @@
import { useState, useEffect, useRef } from "react";
import { usePrivy, useSolanaWallets } from "@privy-io/react-auth";
import { Connection, PublicKey } from "@solana/web3.js"; // Solana Web3.js imports
import { Connection, PublicKey } from "@solana/web3.js";
import { toast } from "sonner";
import "react-toastify/dist/ReactToastify.css";
import { CLUSTER_URL } from "@/data/shared";
import { useFundWallet } from "@privy-io/react-auth/solana";
export default function PrivyButton() {
const { login, logout, user } = usePrivy();
const { login, logout, user, linkTwitter, unlinkTwitter } = usePrivy();
const { fundWallet} = useFundWallet();
const { wallets } = useSolanaWallets();
const [isModalOpen, setIsModalOpen] = useState(false);
const [solWallet, setSolWallet] = useState("");
const [solBalance, setSolBalance] = useState("--");
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 fetchSolBalance = async () => {
wallets.forEach((wallet)=>{
console.log(wallet.address + " : " + wallet.type);
if(wallet.type == "solana"){
const updateSolWallet = ()=>{
wallets.forEach((wallet) => {
if (wallet.type === "solana") {
setSolWallet(wallet.address);
}
})
if (solWallet!="") {
const walletAddress = solWallet; // Access the Solana wallet address
});
}
const fetchSolBalance = async () => {
updateSolWallet();
if (solWallet !== "") {
try {
const connection = new Connection(CLUSTER_URL, "confirmed");
const publicKey = new PublicKey(walletAddress);
const publicKey = new PublicKey(solWallet);
const balance = await connection.getBalance(publicKey);
setSolBalance((balance / 1e9).toFixed(2)); // Convert lamports to SOL (1 SOL = 1e9 lamports)
setSolBalance((balance / 1e9).toFixed(2));
} catch (error) {
console.error("Failed to get balance:", error);
setSolBalance("Error");
}
}
};
const saveProfileChanges = async () => {
if (!user) {
toast.error("User not found!");
return;
}
const updateUrl = `https://vps.playpoolstudios.com/duelfi/api/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 = `https://vps.playpoolstudios.com/duelfi/api/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 = `https://vps.playpoolstudios.com/duelfi/profile_pics/${user.id}.jpg`;
const profilePictureUrl = user?.twitter?.profilePictureUrl ?? customProfileUrl;
if (profilePictureUrl) {
const updatePicUrlApi = `https://vps.playpoolstudios.com/duelfi/api/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]); // Added wallets to dependency array
}, [user, wallets]);
// Function to validate if the address is a valid Solana address (base58)
// Close modal when clicking outside
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (modalRef.current && !modalRef.current.contains(event.target as Node)) {
@ -63,52 +116,207 @@ export default function PrivyButton() {
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 () => {
const walletAddress = solWallet ?? ""; // Use an empty string if wallet address is undefined or null
if (walletAddress) {
await navigator.clipboard.writeText(walletAddress);
toast.success("Wallet address copied!"); // Toast notification for successful copy
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('https://vps.playpoolstudios.com/duelfi/api/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 = `https://vps.playpoolstudios.com/duelfi/api/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 = `https://vps.playpoolstudios.com/duelfi/api/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 = `https://vps.playpoolstudios.com/duelfi/profile_pics/${user?.id}.jpg`;
return (
<>
{user ? (
<button
onClick={() =>{setIsModalOpen(true); fetchSolBalance();}}
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)}
{solWallet?.slice(0, 6)}...{solWallet?.slice(-4)}
</p>
</button>
) : (
<button
onClick={customLogin}
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-105"
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"
>
Sign In
</button>
)}
{/* Modal */}
{isModalOpen && user ? (
{isModalOpen && user && (
<div className="fixed inset-0 bg-black/70 flex justify-center items-start pt-10 z-50">
<div
ref={modalRef}
className="bg-[rgb(30,30,30)] text-white w-full max-w-md p-6 rounded-2xl shadow-lg transform transition-transform duration-300 animate-slide-down"
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"
>
<h2 className="text-xl font-bold mb-4">Account Info</h2>
<h2 className="text-2xl font-bold mb-2">Your Profile</h2>
{/* Wallet Address with Copy Button */}
<div className="mb-4">
<p className="text-gray-400 text-xs font-mono">Connected Wallet</p>
{/* Avatar + Link Twitter Row */}
<div className="flex items-center justify-between gap-4 flex-wrap">
<div className="flex items-center gap-4">
<img
src={user?.twitter ? (twitterProfilePic) : (avatar ?? customProfileUrl)}
alt=""
className="w-16 h-16 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">
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"
>
Unlink
</button>
</div>
) : (
<button
onClick={linkTwitter}
className="bg-black text-white px-4 py-2 rounded-md text-sm hover:bg-gray-900"
>
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"
onClick={saveProfileChanges}
>
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">
<p className="font-mono text-sm truncate">{solWallet}</p>
<button
@ -118,37 +326,18 @@ export default function PrivyButton() {
📋
</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>
<button
onClick={()=>{fundWallet(solWallet, {})}}
className="w-full bg-purple-600 hover:bg-purple-700 text-white font-semibold py-2 rounded-xl transition"
>
Fund Wallet
</button>
</div>
{/* SOL Balance */}
<div className="mb-4">
<p className="text-gray-400 text-xs font-mono">SOL Balance</p>
<p className="text-sm">{solBalance} SOL</p>
</div>
{/* Socials */}
{(user?.discord?.username || user?.email?.address) && (
<div className="mb-4">
<p className="text-gray-400 text-xs font-mono">Socials</p>
<div className="flex gap-2">
{user?.discord?.username && (
<a
href={`https://discord.com/users/${user?.discord?.username}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 text-sm hover:underline"
>
Discord
</a>
)}
{user?.email?.address && (
<span className="text-gray-400 text-sm">{user?.email?.address}</span>
)}
</div>
</div>
)}
{/* Sign Out Button */}
<button
className="mt-6 w-full bg-[rgb(248,144,22)] text-black font-semibold py-2 rounded-xl transition hover:bg-white"
onClick={() => {
@ -160,24 +349,30 @@ export default function PrivyButton() {
</button>
</div>
</div>
) : null}
)}
{/* 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>
)}
{/* Animation */}
<style jsx>{`
@keyframes slide-down {
from {
transform: translateY(-100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.animate-slide-down {
animation: slide-down 0.3s ease-out;
}
`}</style>
</>
);
}

View File

@ -18,8 +18,14 @@ export default function YourGames() {
// Fetch bets
const updateBets = async () => {
const bets: Bet[] = await fetchOpenBets(wallets[0]);
setMyBets(bets.filter((bet) => bet.owner === wallets[0].address));
let wallet = wallets[0];
wallets.forEach((_wallet) => {
if (wallet.type === "solana") {
wallet = _wallet;
}
});
const bets: Bet[] = await fetchOpenBets(wallet);
setMyBets(bets.filter((bet) => bet.owner === wallet.address));
setLoading(false);
};
@ -34,8 +40,14 @@ export default function YourGames() {
const handleCloseBet = async () => {
if (!selectedBet) return;
setIsProcessing(true);
let wallet = wallets[0];
wallets.forEach((_wallet) => {
if (wallet.type === "solana") {
wallet = _wallet;
}
});
try {
const tx = await closeBet(wallets[0], selectedBet.id);
const tx = await closeBet(wallet, selectedBet.id);
const url = EXPLORER_TX_TEMPLATE.replace("{address}", tx);
toast.success(`Closed the bet successfully!`, {

View File

@ -5,295 +5,310 @@
* IDL can be found at `target/idl/bets.json`.
*/
export type Bets = {
"address": "HxsDuhD7wcPxcMsrYdteMYxkffuwff8HoxhZ7NuFtM37",
"metadata": {
"name": "bets",
"version": "0.1.0",
"spec": "0.1.0",
"description": "Created with Anchor"
"address": "JAf3ZkQ469okXAzA6BKJeKBb9ZkCtZanULaUsapskoyn",
"metadata": {
"name": "bets",
"version": "0.1.0",
"spec": "0.1.0",
"description": "Created with Anchor"
},
"instructions": [
{
"name": "closeBet",
"discriminator": [
185,
206,
13,
184,
176,
108,
140,
107
],
"accounts": [
{
"name": "betsList",
"writable": true
},
{
"name": "betVault",
"writable": true
},
{
"name": "winner",
"writable": true
},
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "systemProgram",
"address": "11111111111111111111111111111111"
}
],
"args": [
{
"name": "winner",
"type": "pubkey"
}
]
},
"instructions": [
{
"name": "closeBet",
"discriminator": [
185,
206,
13,
184,
176,
108,
140,
107
],
"accounts": [
{
"name": "betsList",
"writable": true
},
{
"name": "betVault",
"writable": true
},
{
"name": "winner",
"writable": true
},
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "systemProgram",
"address": "11111111111111111111111111111111"
{
"name": "createBet",
"discriminator": [
197,
42,
153,
2,
59,
63,
143,
246
],
"accounts": [
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "betsList",
"writable": true
},
{
"name": "betVault",
"writable": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
98,
101,
116,
95,
118,
97,
117,
108,
116
]
},
{
"kind": "account",
"path": "payer"
},
{
"kind": "arg",
"path": "gameId"
},
{
"kind": "arg",
"path": "nonce"
}
]
}
],
"args": [
},
{
"name": "systemProgram",
"address": "11111111111111111111111111111111"
}
],
"args": [
{
"name": "wager",
"type": "u64"
},
{
"name": "userId",
"type": "string"
},
{
"name": "gameId",
"type": "string"
},
{
"name": "nonce",
"type": "u64"
}
]
},
{
"name": "initialize",
"discriminator": [
175,
175,
109,
31,
13,
152,
155,
237
],
"accounts": [
{
"name": "betsList",
"writable": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
98,
101,
116,
115,
95,
108,
105,
115,
116
]
}
]
}
},
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "systemProgram",
"address": "11111111111111111111111111111111"
}
],
"args": []
},
{
"name": "joinBet",
"discriminator": [
69,
116,
82,
26,
144,
192,
58,
238
],
"accounts": [
{
"name": "betVault",
"writable": true
},
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "systemProgram",
"address": "11111111111111111111111111111111"
}
],
"args": [
{
"name": "userId",
"type": "string"
},
{
"name": "gameId",
"type": "string"
}
]
}
],
"accounts": [
{
"name": "betVault",
"discriminator": [
103,
78,
21,
234,
18,
250,
230,
209
]
},
{
"name": "betsList",
"discriminator": [
231,
234,
50,
58,
81,
179,
239,
117
]
}
],
"errors": [
{
"code": 6000,
"name": "betNotFilled",
"msg": "Bet is not filled yet!"
}
],
"types": [
{
"name": "betVault",
"type": {
"kind": "struct",
"fields": [
{
"name": "winner",
"name": "gameId",
"type": "string"
},
{
"name": "owner",
"type": "pubkey"
}
]
},
{
"name": "createBet",
"discriminator": [
197,
42,
153,
2,
59,
63,
143,
246
],
"accounts": [
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "betsList",
"writable": true
"name": "ownerId",
"type": "string"
},
{
"name": "betVault",
"writable": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
98,
101,
116,
95,
118,
97,
117,
108,
116
]
},
{
"kind": "account",
"path": "payer"
},
{
"kind": "arg",
"path": "gameId"
},
{
"kind": "arg",
"path": "nonce"
}
]
}
"name": "joiner",
"type": "pubkey"
},
{
"name": "systemProgram",
"address": "11111111111111111111111111111111"
}
],
"args": [
"name": "joinerId",
"type": "string"
},
{
"name": "wager",
"type": "u64"
},
{
"name": "gameId",
"type": "string"
},
{
"name": "nonce",
"type": "u64"
}
]
},
{
"name": "initialize",
"discriminator": [
175,
175,
109,
31,
13,
152,
155,
237
],
"accounts": [
}
},
{
"name": "betsList",
"type": {
"kind": "struct",
"fields": [
{
"name": "betsList",
"writable": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
98,
101,
116,
115,
95,
108,
105,
115,
116
]
}
]
"name": "bets",
"type": {
"vec": "pubkey"
}
},
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "systemProgram",
"address": "11111111111111111111111111111111"
}
],
"args": []
},
{
"name": "joinBet",
"discriminator": [
69,
116,
82,
26,
144,
192,
58,
238
],
"accounts": [
{
"name": "betVault",
"writable": true
},
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "systemProgram",
"address": "11111111111111111111111111111111"
}
],
"args": [
{
"name": "gameId",
"type": "string"
}
]
}
],
"accounts": [
{
"name": "betVault",
"discriminator": [
103,
78,
21,
234,
18,
250,
230,
209
]
},
{
"name": "betsList",
"discriminator": [
231,
234,
50,
58,
81,
179,
239,
117
]
}
],
"errors": [
{
"code": 6000,
"name": "customError",
"msg": "Custom error message"
}
],
"types": [
{
"name": "betVault",
"type": {
"kind": "struct",
"fields": [
{
"name": "gameId",
"type": "string"
},
{
"name": "owner",
"type": "pubkey"
},
{
"name": "joiner",
"type": "pubkey"
},
{
"name": "wager",
"type": "u64"
}
]
}
},
{
"name": "betsList",
"type": {
"kind": "struct",
"fields": [
{
"name": "bets",
"type": {
"vec": "pubkey"
}
}
]
}
}
],
"constants": [
{
"name": "seed",
"type": "string",
"value": "\"anchor\""
}
]
};
}
],
"constants": [
{
"name": "seed",
"type": "string",
"value": "\"anchor\""
}
]
};

View File

@ -1,292 +1,308 @@
{
"address": "HxsDuhD7wcPxcMsrYdteMYxkffuwff8HoxhZ7NuFtM37",
"metadata": {
"name": "bets",
"version": "0.1.0",
"spec": "0.1.0",
"description": "Created with Anchor"
"address": "JAf3ZkQ469okXAzA6BKJeKBb9ZkCtZanULaUsapskoyn",
"metadata": {
"name": "bets",
"version": "0.1.0",
"spec": "0.1.0",
"description": "Created with Anchor"
},
"instructions": [
{
"name": "close_bet",
"discriminator": [
185,
206,
13,
184,
176,
108,
140,
107
],
"accounts": [
{
"name": "bets_list",
"writable": true
},
{
"name": "bet_vault",
"writable": true
},
{
"name": "winner",
"writable": true
},
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "system_program",
"address": "11111111111111111111111111111111"
}
],
"args": [
{
"name": "winner",
"type": "pubkey"
}
]
},
"instructions": [
{
"name": "close_bet",
"discriminator": [
185,
206,
13,
184,
176,
108,
140,
107
],
"accounts": [
{
"name": "bets_list",
"writable": true
},
{
"name": "bet_vault",
"writable": true
},
{
"name": "winner",
"writable": true
},
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "system_program",
"address": "11111111111111111111111111111111"
{
"name": "create_bet",
"discriminator": [
197,
42,
153,
2,
59,
63,
143,
246
],
"accounts": [
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "bets_list",
"writable": true
},
{
"name": "bet_vault",
"writable": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
98,
101,
116,
95,
118,
97,
117,
108,
116
]
},
{
"kind": "account",
"path": "payer"
},
{
"kind": "arg",
"path": "game_id"
},
{
"kind": "arg",
"path": "_nonce"
}
]
}
],
"args": [
},
{
"name": "system_program",
"address": "11111111111111111111111111111111"
}
],
"args": [
{
"name": "wager",
"type": "u64"
},
{
"name": "user_id",
"type": "string"
},
{
"name": "game_id",
"type": "string"
},
{
"name": "nonce",
"type": "u64"
}
]
},
{
"name": "initialize",
"discriminator": [
175,
175,
109,
31,
13,
152,
155,
237
],
"accounts": [
{
"name": "bets_list",
"writable": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
98,
101,
116,
115,
95,
108,
105,
115,
116
]
}
]
}
},
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "system_program",
"address": "11111111111111111111111111111111"
}
],
"args": []
},
{
"name": "join_bet",
"discriminator": [
69,
116,
82,
26,
144,
192,
58,
238
],
"accounts": [
{
"name": "bet_vault",
"writable": true
},
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "system_program",
"address": "11111111111111111111111111111111"
}
],
"args": [
{
"name": "user_id",
"type": "string"
},
{
"name": "game_id",
"type": "string"
}
]
}
],
"accounts": [
{
"name": "BetVault",
"discriminator": [
103,
78,
21,
234,
18,
250,
230,
209
]
},
{
"name": "BetsList",
"discriminator": [
231,
234,
50,
58,
81,
179,
239,
117
]
}
],
"errors": [
{
"code": 6000,
"name": "BetNotFilled",
"msg": "Bet is not filled yet!"
}
],
"types": [
{
"name": "BetVault",
"type": {
"kind": "struct",
"fields": [
{
"name": "winner",
"name": "game_id",
"type": "string"
},
{
"name": "owner",
"type": "pubkey"
}
]
},
{
"name": "create_bet",
"discriminator": [
197,
42,
153,
2,
59,
63,
143,
246
],
"accounts": [
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "bets_list",
"writable": true
"name": "owner_id",
"type": "string"
},
{
"name": "bet_vault",
"writable": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
98,
101,
116,
95,
118,
97,
117,
108,
116
]
},
{
"kind": "account",
"path": "payer"
},
{
"kind": "arg",
"path": "game_id"
},
{
"kind": "arg",
"path": "_nonce"
}
]
}
"name": "joiner",
"type": "pubkey"
},
{
"name": "system_program",
"address": "11111111111111111111111111111111"
}
],
"args": [
"name": "joiner_id",
"type": "string"
},
{
"name": "wager",
"type": "u64"
},
{
"name": "game_id",
"type": "string"
},
{
"name": "nonce",
"type": "u64"
}
]
},
{
"name": "initialize",
"discriminator": [
175,
175,
109,
31,
13,
152,
155,
237
],
"accounts": [
}
},
{
"name": "BetsList",
"type": {
"kind": "struct",
"fields": [
{
"name": "bets_list",
"writable": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
98,
101,
116,
115,
95,
108,
105,
115,
116
]
}
]
"name": "bets",
"type": {
"vec": "pubkey"
}
},
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "system_program",
"address": "11111111111111111111111111111111"
}
],
"args": []
},
{
"name": "join_bet",
"discriminator": [
69,
116,
82,
26,
144,
192,
58,
238
],
"accounts": [
{
"name": "bet_vault",
"writable": true
},
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "system_program",
"address": "11111111111111111111111111111111"
}
],
"args": [
{
"name": "game_id",
"type": "string"
}
]
}
],
"accounts": [
{
"name": "BetVault",
"discriminator": [
103,
78,
21,
234,
18,
250,
230,
209
]
},
{
"name": "BetsList",
"discriminator": [
231,
234,
50,
58,
81,
179,
239,
117
]
}
],
"errors": [
{
"code": 6000,
"name": "CustomError",
"msg": "Custom error message"
}
],
"types": [
{
"name": "BetVault",
"type": {
"kind": "struct",
"fields": [
{
"name": "game_id",
"type": "string"
},
{
"name": "owner",
"type": "pubkey"
},
{
"name": "joiner",
"type": "pubkey"
},
{
"name": "wager",
"type": "u64"
}
]
}
},
{
"name": "BetsList",
"type": {
"kind": "struct",
"fields": [
{
"name": "bets",
"type": {
"vec": "pubkey"
}
}
]
}
}
],
"constants": [
{
"name": "SEED",
"type": "string",
"value": "\"anchor\""
}
]
}
}
],
"constants": [
{
"name": "SEED",
"type": "string",
"value": "\"anchor\""
}
]
}

View File

@ -0,0 +1,6 @@
export async function fetchUserById(id: string) {
const res = await fetch(`https://vps.playpoolstudios.com/duelfi/api/get_user_by_id.php?id=${id}`);
if (!res.ok) return null;
return await res.json();
}

View File

@ -8,7 +8,6 @@ import { Bet } from "@/types/Bet";
import { toast } from "sonner";
import { Game } from "@/types/Game";
export const fetchOpenBets = async (wallets: ConnectedSolanaWallet): Promise<Bet[]> => {
try {
if (!wallets) return [];
@ -39,7 +38,9 @@ export const fetchOpenBets = async (wallets: ConnectedSolanaWallet): Promise<Bet
return {
id: betAcc.gameId,
owner: betAcc.owner.toBase58(),
owner_id:betAcc.ownerId,
joiner: betAcc.joiner ? betAcc.joiner.toBase58() : "Open",
joiner_id:betAcc.joinerId,
wager: betAcc.wager.toNumber() / LAMPORTS_PER_SOL
};
})
@ -120,7 +121,7 @@ export const fetchOpenBets = async (wallets: ConnectedSolanaWallet): Promise<Bet
}
export async function createBet(wallets:ConnectedSolanaWallet,selectedPrice:number,selectedGame:Game):Promise<string>{
export async function createBet(wallets:ConnectedSolanaWallet, uid:string,selectedPrice:number,selectedGame:Game):Promise<string>{
const connection = new Connection(CLUSTER_URL);
const wallet = {
publicKey: new PublicKey(wallets.address),
@ -134,7 +135,7 @@ export async function createBet(wallets:ConnectedSolanaWallet,selectedPrice:numb
const program = new Program<Bets>(idl, provider);
try {
const nonce = 1;
const nonce = getRandomInt(100000000);
const [bet_list_pda] = await PublicKey.findProgramAddress(
[Buffer.from("bets_list")],
program.programId
@ -145,7 +146,7 @@ export async function createBet(wallets:ConnectedSolanaWallet,selectedPrice:numb
// Create transaction
const tx = await program.methods
.createBet(new BN(selectedPrice * 1000000000), selectedGame.id, new BN(nonce))
.createBet(new BN(selectedPrice * 1000000000),uid, selectedGame.id, new BN(nonce))
.accounts({
betsList: bet_list_pda,
})
@ -177,3 +178,7 @@ export async function createBet(wallets:ConnectedSolanaWallet,selectedPrice:numb
return "";
}
function getRandomInt(max:number):number {
return Math.floor(Math.random() * max);
}

View File

@ -1,6 +1,14 @@
export interface Bet {
id: string;
owner: string;
owner_id:string;
joiner: string;
joiner_id:string;
wager: number;
ownerProfile?: {
id: string;
username: string;
bio: string;
x_profile_url: string;
};
}