dino_landing_page/app/page.tsx
2025-08-17 19:42:53 +05:30

386 lines
14 KiB
TypeScript

'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() {
const { publicKey, sendTransaction } = useWallet();
const [isProcessing, setIsProcessing] = useState(false);
const [txHash, setTxHash] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [hasTicket, setHasTicket] = useState(false);
const [ticketTxHash, setTicketTxHash] = useState<string | null>(null);
const [showPaymentSuccess, setShowPaymentSuccess] = useState(false);
const [highscore, setHighscore] = useState(0);
const [dashboardData, setDashboardData] = useState<DashboardData>({ leaderboard: [], available_tx: null, attempts_count: 0 });
const [dashboardLoading, setDashboardLoading] = useState(true);
const [dashboardError, setDashboardError] = useState<string | null>(null);
const headerRef = useRef<HeaderRef>(null);
// Poll for available tickets every 5 seconds (only when no ticket is available)
useEffect(() => {
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 (
<div className="min-h-screen">
<Header ref={headerRef} />
{/* Game iframe - shown when user has a ticket */}
{hasTicket && (
<div className="w-full h-screen flex justify-center items-center bg-white">
<iframe
src={`/Build/index.html?tx=${ticketTxHash}&highscore=${highscore}`}
className="w-full h-full"
title="Dino Game"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
</div>
)}
{/* Main content - only shown when no ticket */}
{!hasTicket && (
<main className="pt-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="text-center">
<h2 className="text-4xl font-bold text-gray-900 mb-4">
Welcome to <span className="text-green-600">$DINO</span>
</h2>
<p className="text-xl text-gray-600 max-w-2xl mx-auto mb-8">
Beat the highscore to win the jackpot!
</p>
{/* Enter Button - only show when no ticket */}
{!hasTicket && (
<div className="flex flex-col items-center space-y-4">
<button
onClick={handleEnterGame}
disabled={!publicKey || isProcessing}
className={`px-8 py-4 text-xl font-bold rounded-xl shadow-lg transition-all duration-300 transform hover:scale-105 hover:-translate-y-1 ${
!publicKey
? 'bg-gray-400 cursor-not-allowed'
: isProcessing
? 'bg-yellow-500 cursor-wait'
: 'bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-white shadow-green-500/25 hover:shadow-xl hover:shadow-green-500/30'
}`}
>
{!publicKey
? 'Connect Wallet to Enter'
: isProcessing
? 'Processing Payment...'
: `Enter Game - Pay ${ENTRY_FEE_DINO} $DINO`
}
</button>
{/* Status Messages */}
{error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded-lg max-w-md">
<p className="text-sm">{error}</p>
</div>
)}
{txHash && (
<div
className={`bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded-lg max-w-md transition-all duration-300 ease-in-out transform ${
showPaymentSuccess
? 'opacity-100 scale-100 translate-y-0'
: 'opacity-0 scale-95 translate-y-2'
}`}
>
<p className="text-sm font-semibold mb-2">Payment Successful!</p>
<p className="text-xs break-all">Transaction Hash: {txHash}</p>
<a
href={`https://explorer.solana.com/tx/${txHash}?cluster=devnet`}
target="_blank"
rel="noopener noreferrer"
className="text-xs underline hover:text-green-800"
>
View on Explorer
</a>
</div>
)}
</div>
)}
{/* Leaderboard */}
<div className="mt-12 max-w-2xl mx-auto">
<Leaderboard
leaderboard={dashboardData.leaderboard}
loading={dashboardLoading}
error={dashboardError}
attemptsCount={dashboardData.attempts_count}
/>
</div>
</div>
</div>
</main>
)}
</div>
);
}