From 2cdc49e86a3ade1dd8ed312f2f9b936c3b8884c8 Mon Sep 17 00:00:00 2001 From: Sewmina Date: Sun, 17 Aug 2025 19:42:53 +0530 Subject: [PATCH] init --- README.md | 200 +- app/components/Header.tsx | 279 + app/components/Leaderboard.tsx | 166 + app/components/WalletProvider.tsx | 41 + app/constants.ts | 5 + app/favicon.ico | Bin 25931 -> 270398 bytes app/globals.css | 66 +- app/layout.tsx | 24 +- app/page.tsx | 474 +- app/shared.ts | 7 + package-lock.json | 14818 +++++++++++++++- package.json | 20 +- postcss.config.mjs | 5 +- public/Build/Build/prod.data | Bin 0 -> 5794518 bytes public/Build/Build/prod.framework.js | 22 + public/Build/Build/prod.loader.js | 1 + public/Build/Build/prod.wasm | Bin 0 -> 22301126 bytes public/Build/TemplateData/MemoryProfiler.png | Bin 0 -> 665 bytes public/Build/TemplateData/favicon.ico | Bin 0 -> 2305 bytes .../Build/TemplateData/fullscreen-button.png | Bin 0 -> 175 bytes .../TemplateData/progress-bar-empty-dark.png | Bin 0 -> 96 bytes .../TemplateData/progress-bar-empty-light.png | Bin 0 -> 109 bytes .../TemplateData/progress-bar-full-dark.png | Bin 0 -> 74 bytes .../TemplateData/progress-bar-full-light.png | Bin 0 -> 84 bytes public/Build/TemplateData/style.css | 16 + public/Build/TemplateData/unity-logo-dark.png | Bin 0 -> 3042 bytes .../Build/TemplateData/unity-logo-light.png | Bin 0 -> 3077 bytes public/Build/TemplateData/webgl-logo.png | Bin 0 -> 2947 bytes public/Build/TemplateData/webmemd-icon.png | Bin 0 -> 1670 bytes public/Build/index.html | 135 + public/dino_icon.jpg | Bin 0 -> 7605 bytes public/game.html | 68 + tailwind.config.ts | 34 + 33 files changed, 15557 insertions(+), 824 deletions(-) create mode 100644 app/components/Header.tsx create mode 100644 app/components/Leaderboard.tsx create mode 100644 app/components/WalletProvider.tsx create mode 100644 app/constants.ts create mode 100644 app/shared.ts create mode 100644 public/Build/Build/prod.data create mode 100644 public/Build/Build/prod.framework.js create mode 100644 public/Build/Build/prod.loader.js create mode 100644 public/Build/Build/prod.wasm create mode 100644 public/Build/TemplateData/MemoryProfiler.png create mode 100644 public/Build/TemplateData/favicon.ico create mode 100644 public/Build/TemplateData/fullscreen-button.png create mode 100644 public/Build/TemplateData/progress-bar-empty-dark.png create mode 100644 public/Build/TemplateData/progress-bar-empty-light.png create mode 100644 public/Build/TemplateData/progress-bar-full-dark.png create mode 100644 public/Build/TemplateData/progress-bar-full-light.png create mode 100644 public/Build/TemplateData/style.css create mode 100644 public/Build/TemplateData/unity-logo-dark.png create mode 100644 public/Build/TemplateData/unity-logo-light.png create mode 100644 public/Build/TemplateData/webgl-logo.png create mode 100644 public/Build/TemplateData/webmemd-icon.png create mode 100644 public/Build/index.html create mode 100644 public/dino_icon.jpg create mode 100644 public/game.html create mode 100644 tailwind.config.ts diff --git a/README.md b/README.md index e215bc4..2c65f03 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,194 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# 🦖 Dino Game - Solana Token Website -## Getting Started +A modern, dino-themed website for your Chrome offline dino game, integrated with Solana blockchain for token-based gameplay. -First, run the development server: +## ✨ Features +- **Wallet Integration**: Connect Phantom and other Solana wallets +- **Token Payment**: Pay 1 DINO token to play the game +- **Buy Token Button**: Purchase DINO tokens directly from the website +- **Game Integration**: Seamless iframe integration for your Unity game +- **Dino Theme**: Beautiful green and yellow color scheme matching the game +- **Responsive Design**: Works on all devices + +## 🚀 Getting Started + +### Prerequisites + +- Node.js 20.16.0 or higher +- npm or yarn package manager +- Solana wallet (Phantom recommended) + +### Installation + +1. Clone the repository: ```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev +git clone +cd dino_landing_page ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +2. Install dependencies: +```bash +npm install +``` -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +3. Start the development server: +```bash +npm run dev +``` -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +4. Open [http://localhost:3000](http://localhost:3000) in your browser. -## Learn More +## 🔧 Configuration -To learn more about Next.js, take a look at the following resources: +### Update Your Wallet Address -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +In `app/components/GameSection.tsx`, replace the placeholder wallet address: -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +```typescript +// Replace with your actual wallet address +const RECIPIENT_WALLET = new PublicKey('YOUR_ACTUAL_WALLET_ADDRESS_HERE'); +``` -## Deploy on Vercel +### Customize Token Amount -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +Currently set to 0.001 SOL for demo purposes. To implement actual DINO token transfers: -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +1. Create your DINO token on Solana +2. Update the transaction logic in `GameSection.tsx` +3. Use SPL Token Program for token transfers + +### Integrate Your Unity Game + +1. Export your Unity game to WebGL +2. Replace `public/game.html` with your game's `index.html` +3. Ensure your game works in an iframe +4. Test the integration + +## 🎮 How It Works + +1. **Connect Wallet**: Users connect their Solana wallet +2. **Buy Tokens**: Purchase DINO tokens (integrate with your preferred DEX) +3. **Pay to Play**: Send 1 DINO token to your wallet +4. **Game Launch**: Game opens in an iframe after successful payment +5. **Blockchain Integration**: All transactions are recorded on Solana + +## 🎨 Customization + +### Colors + +Update the CSS variables in `app/globals.css`: + +```css +:root { + --dino-green: #059669; + --dino-green-light: #10b981; + --dino-green-dark: #047857; + --dino-yellow: #eab308; + --dino-yellow-light: #fbbf24; + --dino-yellow-dark: #ca8a04; +} +``` + +### Styling + +- Modify Tailwind classes in components +- Update CSS animations and transitions +- Customize button styles and hover effects + +## 🔌 DEX Integration + +To enable actual token purchases, integrate with: + +- **Raydium**: Popular Solana DEX +- **Orca**: User-friendly DEX +- **Jupiter**: Best price aggregator +- **Metaplex**: NFT marketplace + +Example Raydium integration: + +```typescript +// In your buy token function +const raydiumUrl = `https://raydium.io/swap/?inputCurrency=sol&outputCurrency=${DINO_TOKEN_MINT}`; +window.open(raydiumUrl, '_blank'); +``` + +## 🚀 Deployment + +### Vercel (Recommended) + +1. Push your code to GitHub +2. Connect your repository to Vercel +3. Deploy automatically on push + +### Other Platforms + +- **Netlify**: Drag and drop deployment +- **AWS Amplify**: Full-stack deployment +- **Heroku**: Traditional hosting + +## 📱 Mobile Optimization + +The website is fully responsive and includes: + +- Touch-friendly buttons +- Mobile-optimized layouts +- Responsive iframe sizing +- Mobile wallet support + +## 🔒 Security Considerations + +- Always verify wallet connections +- Implement proper transaction validation +- Use HTTPS in production +- Validate iframe sources +- Implement rate limiting + +## 🐛 Troubleshooting + +### Common Issues + +1. **Wallet not connecting**: Ensure wallet extension is installed +2. **Transaction failing**: Check SOL balance for fees +3. **Game not loading**: Verify iframe source and CORS settings +4. **Build errors**: Check Node.js version compatibility + +### Debug Mode + +Enable debug logging in development: + +```typescript +// Add to components for debugging +console.log('Wallet connected:', publicKey?.toString()); +console.log('Transaction status:', transactionStatus); +``` + +## 📚 Resources + +- [Solana Documentation](https://docs.solana.com/) +- [Wallet Adapter](https://github.com/solana-labs/wallet-adapter) +- [Web3.js](https://solana-labs.github.io/solana-web3.js/) +- [Tailwind CSS](https://tailwindcss.com/) + +## 🤝 Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Submit a pull request + +## 📄 License + +This project is licensed under the MIT License - see the LICENSE file for details. + +## 🆘 Support + +For support and questions: + +- Create an issue in the repository +- Contact the development team +- Check the documentation + +--- + +**Happy Gaming! 🦖🎮** diff --git a/app/components/Header.tsx b/app/components/Header.tsx new file mode 100644 index 0000000..fd48042 --- /dev/null +++ b/app/components/Header.tsx @@ -0,0 +1,279 @@ +'use client'; + +import { useState, useEffect, useImperativeHandle, forwardRef } from 'react'; +import { useWallet } from '@solana/wallet-adapter-react'; +import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'; +import { Connection, PublicKey } from '@solana/web3.js'; +import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; +import Image from 'next/image'; +import '@solana/wallet-adapter-react-ui/styles.css'; +import { CLUSTER_URL } from '../shared'; +import { DINO_TOKEN_ADDRESS } from '../constants'; + +export interface HeaderRef { + refreshBalance: () => Promise; +} + +const Header = forwardRef((props, ref) => { + const { publicKey } = useWallet(); + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + const [isScrolled, setIsScrolled] = useState(false); + const [dinoBalance, setDinoBalance] = useState(null); + const [isLoadingBalance, setIsLoadingBalance] = useState(false); + + const fetchDinoBalance = async () => { + if (!publicKey) { + setDinoBalance(null); + return; + } + + setIsLoadingBalance(true); + try { + const connection = new Connection(CLUSTER_URL); + const tokenMint = new PublicKey(DINO_TOKEN_ADDRESS); + + // Get token accounts for the user + const tokenAccounts = await connection.getParsedTokenAccountsByOwner( + publicKey, + { mint: tokenMint } + ); + + if (tokenAccounts.value.length > 0) { + const balance = tokenAccounts.value[0].account.data.parsed.info.tokenAmount.uiAmount; + setDinoBalance(balance); + } else { + setDinoBalance(0); + } + } catch (error) { + console.error('Error fetching DINO balance:', error); + setDinoBalance(null); + } finally { + setIsLoadingBalance(false); + } + }; + + // Expose refreshBalance function to parent component + useImperativeHandle(ref, () => ({ + refreshBalance: fetchDinoBalance + })); + + useEffect(() => { + const handleScroll = () => { + setIsScrolled(window.scrollY > 10); + }; + + window.addEventListener('scroll', handleScroll); + return () => window.removeEventListener('scroll', handleScroll); + }, []); + + useEffect(() => { + fetchDinoBalance(); + }, [publicKey]); + + const handleBuyDino = () => { + // Redirect to DexScreener - you can update this URL to the actual $DINO token page + window.open('https://dexscreener.com/solana', '_blank'); + }; + + const formatBalance = (balance: number | null) => { + if (balance === null) return '0'; + if (balance === 0) return '0'; + if (balance < 0.0001) return '< 0.0001'; + return balance.toFixed(4); + }; + + return ( +
+
+
+ {/* Logo and Title */} +
+
+
+ DINO Token Icon +
+

+ DINO +

+
+
+ + {/* Desktop Navigation Links */} + + + {/* Buy $DINO Button and Wallet Connect - Desktop */} +
+ + + {publicKey ? ( +
+ {/* DINO Balance Display */} +
+
+
+ + {isLoadingBalance ? ( +
+
+ Loading... +
+ ) : ( + `${formatBalance(dinoBalance)} $DINO` + )} +
+
+
+ + +
+ ) : ( + + )} +
+ + {/* Mobile Menu Button */} +
+ + + +
+
+ + {/* Mobile Menu */} + {isMobileMenuOpen && ( +
+
+ + Support + + + Whitepaper + +
+ Socials: + + + + + + + + + + +
+ {publicKey && ( +
+
+
+
+ + {isLoadingBalance ? ( +
+
+ Loading... +
+ ) : ( + `${formatBalance(dinoBalance)} $DINO` + )} +
+
+
+
+ )} +
+
+ )} +
+
+ ); +}); + +export default Header; diff --git a/app/components/Leaderboard.tsx b/app/components/Leaderboard.tsx new file mode 100644 index 0000000..3209528 --- /dev/null +++ b/app/components/Leaderboard.tsx @@ -0,0 +1,166 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { ENTRY_FEE_DINO, MAX_ATTEMPTS } from '../shared'; + +interface LeaderboardEntry { + owner: string; + score: string; +} + +interface LeaderboardProps { + leaderboard: LeaderboardEntry[]; + loading: boolean; + error: string | null; + attemptsCount: number; +} + +export default function Leaderboard({ leaderboard, loading, error, attemptsCount }: LeaderboardProps) { + + // Only show loading state if we have no data yet + if (loading && leaderboard.length === 0) { + return ( +
+

🏆 Leaderboard

+
+
+
+
+ ); + } + + if (error) { + return ( +
+

🏆 Leaderboard

+
+

Failed to load leaderboard

+ +
+
+ ); + } + + // Sort leaderboard by score (highest first) + const sortedLeaderboard = [...leaderboard].sort((a, b) => + parseInt(b.score) - parseInt(a.score) + ); + + return ( +
+

🏆 Leaderboard

+ + {/* Combined Prize and Attempts Information */} +
+
+

💰 Prize ({MAX_ATTEMPTS * ENTRY_FEE_DINO} DINO Total)

+
+
🎯 Attempts Remaining:
+
{MAX_ATTEMPTS - attemptsCount}
+
+
+
+
+ 🥇 1st Place: + {(MAX_ATTEMPTS * ENTRY_FEE_DINO * 0.8).toFixed(1)} DINO (80%) +
+
+ 🥈 2nd Place: + {(MAX_ATTEMPTS * ENTRY_FEE_DINO * 0.1).toFixed(1)} DINO (10%) +
+
+ 🥉 3rd Place: + {(MAX_ATTEMPTS * ENTRY_FEE_DINO * 0.05).toFixed(1)} DINO (5%) +
+
+
+

Leaderboard resets after {MAX_ATTEMPTS} attempts

+
+
+ + {sortedLeaderboard.length === 0 ? ( +
+

No scores yet. Be the first to play!

+
+ ) : ( +
+ {sortedLeaderboard.map((entry, index) => ( +
+
+
+ {index + 1} +
+
+

+ {entry.owner.length > 20 + ? `${entry.owner.slice(0, 8)}...${entry.owner.slice(-8)}` + : entry.owner + } +

+ {entry.owner.length > 20 && ( +

+ {entry.owner} +

+ )} + {/* Prize indicators for top 3 */} + {index < 3 && ( +
+ + {index === 0 + ? `🥇 ${(MAX_ATTEMPTS * ENTRY_FEE_DINO * 0.8).toFixed(1)} DINO Prize` + : index === 1 + ? `🥈 ${(MAX_ATTEMPTS * ENTRY_FEE_DINO * 0.1).toFixed(1)} DINO Prize` + : `🥉 ${(MAX_ATTEMPTS * ENTRY_FEE_DINO * 0.05).toFixed(1)} DINO Prize` + } + +
+ )} +
+
+
+

{entry.score}

+

points

+
+
+ ))} +
+ )} + +
+
+

+ Updates every 5 seconds • {sortedLeaderboard.length} player{sortedLeaderboard.length !== 1 ? 's' : ''} +

+ {loading && leaderboard.length > 0 && ( +
+
+ Refreshing... +
+ )} +
+
+
+ ); +} diff --git a/app/components/WalletProvider.tsx b/app/components/WalletProvider.tsx new file mode 100644 index 0000000..8f397eb --- /dev/null +++ b/app/components/WalletProvider.tsx @@ -0,0 +1,41 @@ +'use client'; + +import { WalletAdapterNetwork } from '@solana/wallet-adapter-base'; +import { ConnectionProvider, WalletProvider as SolanaWalletProvider } from '@solana/wallet-adapter-react'; +import { WalletModalProvider } from '@solana/wallet-adapter-react-ui'; +import { PhantomWalletAdapter, SolflareWalletAdapter } from '@solana/wallet-adapter-wallets'; +import { clusterApiUrl } from '@solana/web3.js'; +import { useMemo } from 'react'; + +// Import wallet adapter CSS +import '@solana/wallet-adapter-react-ui/styles.css'; + +interface WalletProviderProps { + children: React.ReactNode; +} + +export default function WalletProvider({ children }: WalletProviderProps) { + // The network can be set to 'devnet', 'testnet', or 'mainnet-beta'. + const network = WalletAdapterNetwork.Devnet; + + // You can also provide a custom RPC endpoint. + const endpoint = useMemo(() => clusterApiUrl(network), [network]); + + const wallets = useMemo( + () => [ + new PhantomWalletAdapter(), + new SolflareWalletAdapter(), + ], + [] + ); + + return ( + + + + {children} + + + + ); +} diff --git a/app/constants.ts b/app/constants.ts new file mode 100644 index 0000000..96a16fe --- /dev/null +++ b/app/constants.ts @@ -0,0 +1,5 @@ +import { PublicKey } from "@solana/web3.js"; + +export const DINO_TOKEN_ADDRESS = "diNoZ1L9UEiJMv8i43BjZoPEe2tywwaBTBnKQYf28FT"; +export const FEE_COLLECTOR = new PublicKey("cocD4r4yNpHxPq7CzUebxEMyLki3X4d2Y3HcTX5ptUc"); + \ No newline at end of file diff --git a/app/favicon.ico b/app/favicon.ico index 718d6fea4835ec2d246af9800eddb7ffb276240c..3a93cc4bd0f3445578ace80b8e0aa88a0eb5bde1 100644 GIT binary patch literal 270398 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 literal 25931 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 LrJugUA?W`A8`#=m diff --git a/app/globals.css b/app/globals.css index a2dc41e..ee47795 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,26 +1,50 @@ -@import "tailwindcss"; +@tailwind base; +@tailwind components; +@tailwind utilities; -:root { - --background: #ffffff; - --foreground: #171717; -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); -} - -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; +/* Custom base styles */ +@layer base { + html { + scroll-behavior: smooth; + } + + body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } } -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; +/* Custom component styles */ +@layer components { + .animate-in { + animation: fadeIn 0.3s ease-in-out; + } + + .slide-in-from-top-2 { + animation: slideInFromTop 0.3s ease-out; + } +} + +/* Custom animations */ +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideInFromTop { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } } diff --git a/app/layout.tsx b/app/layout.tsx index f7fa87e..bc4bbf2 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,20 +1,10 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; - -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); +import WalletProvider from "./components/WalletProvider"; export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "DINO - Solana Token", + description: "DINO token on Solana blockchain", }; export default function RootLayout({ @@ -24,10 +14,10 @@ export default function RootLayout({ }>) { return ( - - {children} + + + {children} + ); diff --git a/app/page.tsx b/app/page.tsx index 21b686d..a13fdf9 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,103 +1,385 @@ -import Image from "next/image"; +'use client'; + +import { useState, useRef, useEffect } from 'react'; +import { useWallet } from '@solana/wallet-adapter-react'; +import { Connection, PublicKey, Transaction } from '@solana/web3.js'; +import {createTransferInstruction, getAssociatedTokenAddress, createAssociatedTokenAccountInstruction } from '@solana/spl-token'; +import Header, { HeaderRef } from './components/Header'; +import Leaderboard from './components/Leaderboard'; +import { CLUSTER_URL, ENTRY_FEE_DINO } from './shared'; +import { DINO_TOKEN_ADDRESS, FEE_COLLECTOR } from './constants'; + +interface DashboardData { + leaderboard: Array<{ owner: string; score: string }>; + available_tx: string | null; + attempts_count:number; +} export default function Home() { - return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - app/page.tsx - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
+ const { publicKey, sendTransaction } = useWallet(); + const [isProcessing, setIsProcessing] = useState(false); + const [txHash, setTxHash] = useState(null); + const [error, setError] = useState(null); + const [hasTicket, setHasTicket] = useState(false); + const [ticketTxHash, setTicketTxHash] = useState(null); + const [showPaymentSuccess, setShowPaymentSuccess] = useState(false); + const [highscore, setHighscore] = useState(0); + const [dashboardData, setDashboardData] = useState({ leaderboard: [], available_tx: null, attempts_count: 0 }); + const [dashboardLoading, setDashboardLoading] = useState(true); + const [dashboardError, setDashboardError] = useState(null); + const headerRef = useRef(null); -
- - Vercel logomark { + if (!publicKey) { + setHasTicket(false); + return; + } + + // Don't poll if we already have a ticket + if (hasTicket) { + return; + } + + const checkTicket = async () => { + try { + const response = await fetch(`https://vps.playpoolstudios.com/dino/api/get_available_tx.php?owner=${publicKey.toString()}`); + + if (response.ok) { + const ticketData = await response.text(); + // If we get a valid transaction hash (not "0"), we have a ticket + const ticketAvailable = ticketData !== "0" && ticketData.trim().length > 0; + setHasTicket(ticketAvailable); + + // If ticket becomes available, stop polling immediately + if (ticketAvailable) { + setTicketTxHash(ticketData); + return; + } + + setTicketTxHash(null); + } else { + setHasTicket(false); + setTicketTxHash(null); + } + } catch (error) { + console.error('Error checking ticket:', error); + setHasTicket(false); + } finally { + + } + }; + + // Check immediately + checkTicket(); + + // Set up polling every 5 seconds + const interval = setInterval(checkTicket, 5000); + + // Cleanup interval on unmount, when publicKey changes, or when ticket becomes available + return () => clearInterval(interval); + }, [publicKey, hasTicket]); + + + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + if (event.origin === window.location.origin && typeof event.data === "string") { + try { + const message = JSON.parse(event.data); + if (message?.type === "gameClose" && typeof message.status === "number") { + game_close_signal(message.status); + } + } catch (error) { + console.error("JSON parse error from Unity message:", error); + } + } + }; + + window.addEventListener("message", handleMessage); + return () => window.removeEventListener("message", handleMessage); + }, []); + + // Fetch dashboard data every 5 seconds + useEffect(() => { + const fetchDashboardData = async () => { + try { + setDashboardLoading(true); + const response = await fetch('https://vps.playpoolstudios.com/dino/api/get_dashboard_data.php'); + + if (!response.ok) { + throw new Error('Failed to fetch dashboard data'); + } + + const data: DashboardData = await response.json(); + setDashboardData(data); + setDashboardError(null); + } catch (err) { + console.error('Error fetching dashboard data:', err); + setDashboardError(err instanceof Error ? err.message : 'Failed to fetch dashboard data'); + } finally { + setDashboardLoading(false); + } + }; + + fetchDashboardData(); + + // Refresh dashboard data every 5 seconds + const interval = setInterval(fetchDashboardData, 5000); + + return () => clearInterval(interval); + }, []); + + const game_close_signal = (status: number) => { + console.log("game_close_signal", status); + setHasTicket(false); + setTicketTxHash(null); + }; + + // Auto-hide payment success panel after 5 seconds + useEffect(() => { + console.log('useEffect triggered:', { txHash, showPaymentSuccess }); + + if (txHash && !showPaymentSuccess) { + console.log('Starting progress animation'); + setShowPaymentSuccess(true); + console.log('showPaymentSuccess set to true'); + } else { + console.log('Condition not met:', { txHash: !!txHash, showPaymentSuccess }); + } + }, [txHash, showPaymentSuccess]); + + // Test useEffect + useEffect(() => { + console.log('Test useEffect - component mounted'); + }, []); + + const handleEnterGame = async () => { + if (!publicKey) { + setError('Please connect your wallet first'); + return; + } + + setIsProcessing(true); + setError(null); + setTxHash(null); + + try { + const connection = new Connection(CLUSTER_URL); + const dinoMint = new PublicKey(DINO_TOKEN_ADDRESS); + + // Get the user's DINO token account + const userTokenAccount = await getAssociatedTokenAddress( + dinoMint, + publicKey + ); + + // Get or create the fee collector's DINO token account + const feeCollectorTokenAccount = await getAssociatedTokenAddress( + dinoMint, + FEE_COLLECTOR + ); + + const transaction = new Transaction(); + + // Check if fee collector token account exists, if not create it + const feeCollectorAccountInfo = await connection.getAccountInfo(feeCollectorTokenAccount); + if (!feeCollectorAccountInfo) { + transaction.add( + createAssociatedTokenAccountInstruction( + publicKey, + feeCollectorTokenAccount, + FEE_COLLECTOR, + dinoMint + ) + ); + } + + // Add the transfer instruction (1 DINO token = 1,000,000,000 lamports for 9 decimals) + const transferAmount = ENTRY_FEE_DINO * 1_000_000_000; // 1 DINO token + + transaction.add( + createTransferInstruction( + userTokenAccount, + feeCollectorTokenAccount, + publicKey, + transferAmount + ) + ); + + // Get recent blockhash + const { blockhash } = await connection.getLatestBlockhash(); + transaction.recentBlockhash = blockhash; + transaction.feePayer = publicKey; + + // Send the transaction + const signature = await sendTransaction(transaction, connection); + + // Wait for confirmation + const confirmation = await connection.confirmTransaction(signature, 'confirmed'); + + if (confirmation.value.err) { + throw new Error('Transaction failed'); + } + + setTxHash(signature); + console.log('Transaction successful! Hash:', signature); + console.log('txHash state set to:', signature); + + // Refresh the dino balance in header after successful payment + if (headerRef.current) { + await headerRef.current.refreshBalance(); + } + + // Immediately check for ticket availability after successful payment + try { + const ticketResponse = await fetch(`https://vps.playpoolstudios.com/dino/api/get_available_tx.php?owner=${publicKey.toString()}`); + if (ticketResponse.ok) { + const ticketData = await ticketResponse.text(); + const ticketAvailable = ticketData !== "0" && ticketData.trim().length > 0; + setHasTicket(ticketAvailable); + } + } catch (ticketError) { + console.warn('Error checking ticket after payment:', ticketError); + } + + // Send transaction to validator + try { + const validatorUrl = `https://solpay.playpoolstudios.com/tx/new?tx=${signature}&target_address=${FEE_COLLECTOR.toString()}&amount=1000000000&sender_address=${publicKey.toString()}&token_mint=${dinoMint.toString()}`; + // Add leaderboard entry + const leaderboardUrl = 'https://vps.playpoolstudios.com/dino/api/add_leaderboard.php'; + const leaderboardData = new FormData(); + leaderboardData.append('tx', signature); + leaderboardData.append('owner', publicKey.toString()); + leaderboardData.append('score', '0'); // Initial score of 0 + + try { + const leaderboardResponse = await fetch(leaderboardUrl, { + method: 'POST', + body: leaderboardData + }); + + if (!leaderboardResponse.ok) { + console.warn('Failed to add leaderboard entry'); + } + } catch (leaderboardError) { + console.warn('Error adding leaderboard entry:', leaderboardError); + } + const response = await fetch(validatorUrl); + if (response.ok) { + const result = await response.json(); + console.log('Transaction validated:', result); + } else { + console.warn('Failed to validate transaction with server'); + } + } catch (validationError) { + console.warn('Error validating transaction with server:', validationError); + } + + } catch (err) { + console.error('Error processing payment:', err); + setError(err instanceof Error ? err.message : 'Failed to process payment'); + } finally { + setIsProcessing(false); + } + }; + + return ( +
+
+ + {/* Game iframe - shown when user has a ticket */} + {hasTicket && ( +
+