diff --git a/next.config.ts b/next.config.ts index e9ffa30..1d59d1e 100644 --- a/next.config.ts +++ b/next.config.ts @@ -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; diff --git a/src/app/globals.css b/src/app/globals.css index b0eaa42..3639d11 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -51,4 +51,46 @@ body { linear-gradient(to right, rgba(100, 100, 100, 0.3) 1px, transparent 1px); 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; } \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index 97adbc9..f5444de 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -8,36 +8,39 @@ import { toSolanaWalletConnectors } from "@privy-io/react-auth/solana"; import { Toaster } from "sonner"; export default function Home() { + return ( - +
- -
- -
- + ); } diff --git a/src/components/GameModal.tsx b/src/components/GameModal.tsx index 2a5d732..52c7688 100644 --- a/src/components/GameModal.tsx +++ b/src/components/GameModal.tsx @@ -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(null); const [selectedPrice, setSelectedPrice] = useState(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 ( -
-
e.stopPropagation()}> +
+
e.stopPropagation()} + > {isProcessing ? (
- - - + + +

Processing...

Creating your bet, please wait...

@@ -79,9 +119,15 @@ export default function GameModal({ isOpen, onClose }: GameModalProps) { ) : ( <>

Create Game

- - - + + + isOpen: boolean; + onClose: () => void; +} + +export function HowItWorksModal({ isOpen, onClose }: HowItWorksModalProps) { + const [shouldRender, setShouldRender] = useState(isOpen); + const [animationClass, setAnimationClass] = useState(""); + + 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]); + + if (!shouldRender) return null; + + return ( +
+
e.stopPropagation()} + > +

How It Works

+ +
+ {[ + { 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) => ( +
+

+ {index + 1}. {step} +

+

{desc}

+
+ ))}
+ +
- ); - } - \ No newline at end of file +
+ ); +} diff --git a/src/components/OpenGames.tsx b/src/components/OpenGames.tsx index e72ad00..9d13c82 100644 --- a/src/components/OpenGames.tsx +++ b/src/components/OpenGames.tsx @@ -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([]); 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`); } @@ -32,42 +49,69 @@ export default function YourGames() { {loading ? (

Loading Open games...

- ) : - myBets.length === 0 ? <> :( -
- {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 ? <> : ( +
+ {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 ( +
+ {/* Game Thumbnail */} +
+ {game.name} - return ( -
- {/* Game Thumbnail */} -
- {game.name} + {/* Join Overlay */} +
+ Join +
+
+ + {/* Game Info */} +
+

{game.name}

+ +
+

Wager

+

Prize

+
+ +
+

{bet.wager} SOL

+

{(bet.wager * 2).toFixed(2)} SOL

+
+ + {/* User Info */} + {bet.ownerProfile && ( +
+ {bet.ownerProfile.username} +

{bet.ownerProfile.username}

+
+ )} + +
- - {/* Game Info */} -
-

{game.name}

-

Wager

-

{bet.wager} SOL

-
-
- ); - })} -
- )} + ); + })} +
+ )} ); } diff --git a/src/components/PrivyButton.tsx b/src/components/PrivyButton.tsx index e658528..2de4ef6 100644 --- a/src/components/PrivyButton.tsx +++ b/src/components/PrivyButton.tsx @@ -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 modalRef = useRef(null); + const [solBalance, setSolBalance] = useState("--"); - const fetchSolBalance = async () => { - - wallets.forEach((wallet)=>{ - console.log(wallet.address + " : " + wallet.type); - if(wallet.type == "solana"){ + const [username, setUsername] = useState("Tester"); + const [bio, setBio] = useState(""); + const [avatar, setAvatar] = useState(null); + + const [isUsernameClaimModalOpen, setIsUsernameClaimModalOpen] = useState(false); + const [newUsername, setNewUsername] = useState(""); + + const modalRef = useRef(null); + 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"); } } }; - useEffect(() => { - + 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) => { + 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 ? ( ) : ( )} - {/* Modal */} - {isModalOpen && user ? ( + {isModalOpen && user && (
-

Account Info

+

Your Profile

- {/* Wallet Address with Copy Button */} -
-

Connected Wallet

+ {/* Avatar + Link Twitter Row */} +
+
+ + {(!user.twitter) ? () : (<>)} +
+
+ {user.twitter ? ( +
+ + Connected as {user.twitter.username} + + +
+ ) : ( + + )} +
+
+ {/* Username */} +
+ + setUsername(e.target.value)} + className="w-full bg-gray-800 text-white p-2 rounded-md" + /> +
+ + {/* Bio */} +
+ +