init
200
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 <your-repo-url>
|
||||
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! 🦖🎮**
|
||||
|
|
|
|||
279
app/components/Header.tsx
Normal file
|
|
@ -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<void>;
|
||||
}
|
||||
|
||||
const Header = forwardRef<HeaderRef>((props, ref) => {
|
||||
const { publicKey } = useWallet();
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const [dinoBalance, setDinoBalance] = useState<number | null>(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 (
|
||||
<header className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
|
||||
isScrolled
|
||||
? 'bg-white/95 backdrop-blur-xl shadow-xl shadow-gray-200/50 border-b border-gray-200/40'
|
||||
: 'bg-white/80 backdrop-blur-lg shadow-lg shadow-gray-100/30 border-b border-gray-200/20'
|
||||
}`}>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center h-20">
|
||||
{/* Logo and Title */}
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center space-x-3 group cursor-pointer">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-green-500 to-emerald-600 rounded-xl flex items-center justify-center shadow-lg shadow-green-500/25 transition-all duration-300 group-hover:scale-110 group-hover:shadow-xl group-hover:shadow-green-500/40 overflow-hidden">
|
||||
<Image
|
||||
src="/dino_icon.jpg"
|
||||
alt="DINO Token Icon"
|
||||
width={40}
|
||||
height={40}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<h1 className="text-3xl font-extrabold bg-gradient-to-r from-gray-900 via-gray-800 to-gray-900 bg-clip-text text-transparent transition-all duration-300 group-hover:from-green-600 group-hover:to-emerald-600">
|
||||
DINO
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop Navigation Links */}
|
||||
<nav className="hidden md:flex items-center space-x-10">
|
||||
<a
|
||||
href="#support"
|
||||
className="relative text-gray-700 hover:text-green-600 transition-all duration-300 font-semibold text-lg group"
|
||||
>
|
||||
Support
|
||||
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-gradient-to-r from-green-500 to-emerald-600 transition-all duration-300 group-hover:w-full"></span>
|
||||
</a>
|
||||
<a
|
||||
href="#whitepaper"
|
||||
className="relative text-gray-700 hover:text-green-600 transition-all duration-300 font-semibold text-lg group"
|
||||
>
|
||||
Whitepaper
|
||||
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-gradient-to-r from-green-500 to-emerald-600 transition-all duration-300 group-hover:w-full"></span>
|
||||
</a>
|
||||
<div className="flex items-center space-x-6">
|
||||
<a
|
||||
href="https://x.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-10 h-10 bg-gray-100 hover:bg-green-100 rounded-full flex items-center justify-center transition-all duration-300 group hover:scale-110 hover:shadow-lg hover:shadow-green-500/25 hover:-translate-y-1"
|
||||
>
|
||||
<svg className="w-5 h-5 text-gray-600 group-hover:text-green-600 transition-colors duration-300" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://t.me"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-10 h-10 bg-gray-100 hover:bg-green-100 rounded-full flex items-center justify-center transition-all duration-300 group hover:scale-110 hover:shadow-lg hover:shadow-green-500/25 hover:-translate-y-1"
|
||||
>
|
||||
<svg className="w-5 h-5 text-gray-600 group-hover:text-green-600 transition-colors duration-300" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Buy $DINO Button and Wallet Connect - Desktop */}
|
||||
<div className="hidden md:flex items-center space-x-4">
|
||||
<button
|
||||
onClick={handleBuyDino}
|
||||
className="bg-gradient-to-r from-orange-500 to-red-500 hover:from-orange-600 hover:to-red-600 text-white font-bold py-3 px-6 rounded-xl shadow-lg shadow-orange-500/25 hover:shadow-xl hover:shadow-orange-500/30 transition-all duration-300 hover:scale-105 hover:-translate-y-0.5 flex items-center space-x-2"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
|
||||
</svg>
|
||||
<span>Buy $DINO</span>
|
||||
</button>
|
||||
|
||||
{publicKey ? (
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* DINO Balance Display */}
|
||||
<div className="bg-gradient-to-r from-green-100 to-emerald-50 rounded-full px-4 py-2 border border-green-200 shadow-sm">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-3 h-3 bg-gradient-to-br from-green-500 to-emerald-600 rounded-full"></div>
|
||||
<span className="text-sm text-green-700 font-mono font-medium">
|
||||
{isLoadingBalance ? (
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="w-3 h-3 border-2 border-green-500 border-t-transparent rounded-full animate-spin"></div>
|
||||
<span>Loading...</span>
|
||||
</div>
|
||||
) : (
|
||||
`${formatBalance(dinoBalance)} $DINO`
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WalletMultiButton className="!bg-gradient-to-r !from-green-600 !to-emerald-600 hover:!from-green-700 hover:!to-emerald-700 !text-white !rounded-xl !px-6 !py-3 !text-sm !font-semibold !shadow-lg !shadow-green-500/25 hover:!shadow-xl hover:!shadow-green-500/30 transition-all duration-300 hover:!scale-105 hover:!-translate-y-0.5" />
|
||||
</div>
|
||||
) : (
|
||||
<WalletMultiButton className="!bg-gradient-to-r !from-green-600 !to-emerald-600 hover:!from-green-700 hover:!to-emerald-700 !text-white !rounded-xl !px-6 !py-3 !text-sm !font-semibold !shadow-lg !shadow-green-500/25 hover:!shadow-xl hover:!shadow-green-500/30 transition-all duration-300 hover:!scale-105 hover:!-translate-y-0.5" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<div className="md:hidden flex items-center space-x-3">
|
||||
<button
|
||||
onClick={handleBuyDino}
|
||||
className="bg-gradient-to-r from-orange-500 to-red-500 text-white font-bold py-2 px-4 rounded-lg shadow-lg shadow-orange-500/25 text-sm"
|
||||
>
|
||||
Buy $DINO
|
||||
</button>
|
||||
<WalletMultiButton className="!bg-gradient-to-r !from-green-600 !to-emerald-600 !text-white !rounded-lg !px-4 !py-2 !text-sm !font-semibold !shadow-lg !shadow-green-500/25" />
|
||||
<button
|
||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
className="w-10 h-10 bg-gray-100 hover:bg-green-100 rounded-lg flex items-center justify-center transition-all duration-300 hover:scale-105"
|
||||
>
|
||||
<svg className="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{isMobileMenuOpen ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
) : (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
{isMobileMenuOpen && (
|
||||
<div className="md:hidden border-t border-gray-200/60 bg-white/95 backdrop-blur-sm animate-in slide-in-from-top-2 duration-300">
|
||||
<div className="px-2 pt-2 pb-3 space-y-1">
|
||||
<a
|
||||
href="#support"
|
||||
className="block px-3 py-2 rounded-md text-base font-medium text-gray-700 hover:text-green-600 hover:bg-green-50 transition-colors duration-200"
|
||||
>
|
||||
Support
|
||||
</a>
|
||||
<a
|
||||
href="#whitepaper"
|
||||
className="block px-3 py-2 rounded-md text-base font-medium text-gray-700 hover:text-green-600 hover:bg-green-50 transition-colors duration-200"
|
||||
>
|
||||
Whitepaper
|
||||
</a>
|
||||
<div className="flex items-center space-x-4 px-3 py-2">
|
||||
<span className="text-sm text-gray-500 font-medium">Socials:</span>
|
||||
<a
|
||||
href="https://x.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-8 h-8 bg-gray-100 hover:bg-green-100 rounded-full flex items-center justify-center transition-all duration-300 hover:scale-110"
|
||||
>
|
||||
<svg className="w-4 h-4 text-gray-600" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://t.me"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-8 h-8 bg-gray-100 hover:bg-green-100 rounded-full flex items-center justify-center transition-all duration-300 hover:scale-110"
|
||||
>
|
||||
<svg className="w-4 h-4 text-gray-600" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
{publicKey && (
|
||||
<div className="px-3 py-2">
|
||||
<div className="bg-gradient-to-r from-green-100 to-emerald-50 rounded-lg px-3 py-2 border border-green-200 shadow-sm">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-3 h-3 bg-gradient-to-br from-green-500 to-emerald-600 rounded-full"></div>
|
||||
<span className="text-sm text-green-700 font-mono font-medium">
|
||||
{isLoadingBalance ? (
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="w-3 h-3 border-2 border-green-500 border-t-transparent rounded-full animate-spin"></div>
|
||||
<span>Loading...</span>
|
||||
</div>
|
||||
) : (
|
||||
`${formatBalance(dinoBalance)} $DINO`
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
});
|
||||
|
||||
export default Header;
|
||||
166
app/components/Leaderboard.tsx
Normal file
|
|
@ -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 (
|
||||
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-4">🏆 Leaderboard</h3>
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-green-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-4">🏆 Leaderboard</h3>
|
||||
<div className="text-center py-4">
|
||||
<p className="text-red-600 text-sm">Failed to load leaderboard</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="mt-2 text-green-600 hover:text-green-700 text-sm underline"
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Sort leaderboard by score (highest first)
|
||||
const sortedLeaderboard = [...leaderboard].sort((a, b) =>
|
||||
parseInt(b.score) - parseInt(a.score)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-4">🏆 Leaderboard</h3>
|
||||
|
||||
{/* Combined Prize and Attempts Information */}
|
||||
<div className="mb-6 p-4 bg-gradient-to-r from-green-50 to-emerald-50 rounded-lg border border-green-200">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h4 className="font-semibold text-green-800">💰 Prize ({MAX_ATTEMPTS * ENTRY_FEE_DINO} DINO Total)</h4>
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-blue-700">🎯 Attempts Remaining:</div>
|
||||
<div className="font-bold text-blue-800 text-lg">{MAX_ATTEMPTS - attemptsCount}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-700">🥇 1st Place:</span>
|
||||
<span className="font-bold text-yellow-600">{(MAX_ATTEMPTS * ENTRY_FEE_DINO * 0.8).toFixed(1)} DINO (80%)</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-700">🥈 2nd Place:</span>
|
||||
<span className="font-bold text-gray-600">{(MAX_ATTEMPTS * ENTRY_FEE_DINO * 0.1).toFixed(1)} DINO (10%)</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-700">🥉 3rd Place:</span>
|
||||
<span className="font-bold text-orange-600">{(MAX_ATTEMPTS * ENTRY_FEE_DINO * 0.05).toFixed(1)} DINO (5%)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 pt-3 border-t border-green-200">
|
||||
<p className="text-xs text-blue-600 text-center">Leaderboard resets after {MAX_ATTEMPTS} attempts</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{sortedLeaderboard.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-gray-500">No scores yet. Be the first to play!</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{sortedLeaderboard.map((entry, index) => (
|
||||
<div
|
||||
key={`${entry.owner}-${entry.score}`}
|
||||
className={`flex items-center justify-between p-3 rounded-lg ${
|
||||
index === 0 ? 'bg-yellow-50 border border-yellow-200' :
|
||||
index === 1 ? 'bg-gray-50 border border-gray-200' :
|
||||
index === 2 ? 'bg-orange-50 border border-orange-200' :
|
||||
'bg-gray-50 border border-gray-100'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`flex items-center justify-center w-8 h-8 rounded-full text-sm font-bold ${
|
||||
index === 0 ? 'bg-yellow-400 text-yellow-900' :
|
||||
index === 1 ? 'bg-gray-400 text-gray-900' :
|
||||
index === 2 ? 'bg-orange-400 text-orange-900' :
|
||||
'bg-gray-300 text-gray-700'
|
||||
}`}>
|
||||
{index + 1}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">
|
||||
{entry.owner.length > 20
|
||||
? `${entry.owner.slice(0, 8)}...${entry.owner.slice(-8)}`
|
||||
: entry.owner
|
||||
}
|
||||
</p>
|
||||
{entry.owner.length > 20 && (
|
||||
<p className="text-xs text-gray-500 font-mono">
|
||||
{entry.owner}
|
||||
</p>
|
||||
)}
|
||||
{/* Prize indicators for top 3 */}
|
||||
{index < 3 && (
|
||||
<div className="flex items-center space-x-1 mt-1">
|
||||
<span className={`text-xs px-2 py-1 rounded-full ${
|
||||
index === 0 ? 'bg-yellow-100 text-yellow-700' :
|
||||
index === 1 ? 'bg-gray-100 text-gray-700' :
|
||||
'bg-orange-100 text-orange-700'
|
||||
}`}>
|
||||
{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`
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-lg font-bold text-gray-900">{entry.score}</p>
|
||||
<p className="text-xs text-gray-500">points</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 text-center">
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<p className="text-xs text-gray-500">
|
||||
Updates every 5 seconds • {sortedLeaderboard.length} player{sortedLeaderboard.length !== 1 ? 's' : ''}
|
||||
</p>
|
||||
{loading && leaderboard.length > 0 && (
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="animate-spin rounded-full h-3 w-3 border-b-2 border-green-600"></div>
|
||||
<span className="text-xs text-green-600">Refreshing...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
41
app/components/WalletProvider.tsx
Normal file
|
|
@ -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 (
|
||||
<ConnectionProvider endpoint={endpoint}>
|
||||
<SolanaWalletProvider wallets={wallets} autoConnect>
|
||||
<WalletModalProvider>
|
||||
{children}
|
||||
</WalletModalProvider>
|
||||
</SolanaWalletProvider>
|
||||
</ConnectionProvider>
|
||||
);
|
||||
}
|
||||
5
app/constants.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { PublicKey } from "@solana/web3.js";
|
||||
|
||||
export const DINO_TOKEN_ADDRESS = "diNoZ1L9UEiJMv8i43BjZoPEe2tywwaBTBnKQYf28FT";
|
||||
export const FEE_COLLECTOR = new PublicKey("cocD4r4yNpHxPq7CzUebxEMyLki3X4d2Y3HcTX5ptUc");
|
||||
|
||||
BIN
app/favicon.ico
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 264 KiB |
|
|
@ -1,26 +1,50 @@
|
|||
@import "tailwindcss";
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
/* Custom base styles */
|
||||
@layer base {
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
@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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<body>
|
||||
<WalletProvider>
|
||||
{children}
|
||||
</WalletProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
|
|
|||
460
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 (
|
||||
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
|
||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
priority
|
||||
/>
|
||||
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
|
||||
<li className="mb-2 tracking-[-.01em]">
|
||||
Get started by editing{" "}
|
||||
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
|
||||
app/page.tsx
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li className="tracking-[-.01em]">
|
||||
Save and see your changes instantly.
|
||||
</li>
|
||||
</ol>
|
||||
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);
|
||||
|
||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||
<a
|
||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
// 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
|
||||
/>
|
||||
Deploy now
|
||||
</a>
|
||||
</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
|
||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
href={`https://explorer.solana.com/tx/${txHash}?cluster=devnet`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs underline hover:text-green-800"
|
||||
>
|
||||
Read our docs
|
||||
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>
|
||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/file.svg"
|
||||
alt="File icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Learn
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/window.svg"
|
||||
alt="Window icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Examples
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/globe.svg"
|
||||
alt="Globe icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Go to nextjs.org →
|
||||
</a>
|
||||
</footer>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
7
app/shared.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { clusterApiUrl } from "@solana/web3.js";
|
||||
|
||||
export const CLUSTER_URL = clusterApiUrl('devnet');
|
||||
|
||||
// Game configuration
|
||||
export const ENTRY_FEE_DINO = 10; // Entry fee in DINO tokens
|
||||
export const MAX_ATTEMPTS = 50; // Maximum attempts before leaderboard reset
|
||||
14806
package-lock.json
generated
20
package.json
|
|
@ -9,19 +9,27 @@
|
|||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@solana/spl-token": "^0.4.13",
|
||||
"@solana/wallet-adapter-base": "^0.9.27",
|
||||
"@solana/wallet-adapter-phantom": "^0.9.28",
|
||||
"@solana/wallet-adapter-react": "^0.15.39",
|
||||
"@solana/wallet-adapter-react-ui": "^0.9.39",
|
||||
"@solana/wallet-adapter-wallets": "^0.19.37",
|
||||
"@solana/web3.js": "^1.87.6",
|
||||
"next": "15.4.6",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"next": "15.4.6"
|
||||
"react-dom": "19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"tailwindcss": "^4",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.4.6",
|
||||
"@eslint/eslintrc": "^3"
|
||||
"postcss": "^8.4.32",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
|
|||
BIN
public/Build/Build/prod.data
Normal file
22
public/Build/Build/prod.framework.js
Normal file
1
public/Build/Build/prod.loader.js
Normal file
BIN
public/Build/Build/prod.wasm
Normal file
BIN
public/Build/TemplateData/MemoryProfiler.png
Normal file
|
After Width: | Height: | Size: 665 B |
BIN
public/Build/TemplateData/favicon.ico
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
public/Build/TemplateData/fullscreen-button.png
Normal file
|
After Width: | Height: | Size: 175 B |
BIN
public/Build/TemplateData/progress-bar-empty-dark.png
Normal file
|
After Width: | Height: | Size: 96 B |
BIN
public/Build/TemplateData/progress-bar-empty-light.png
Normal file
|
After Width: | Height: | Size: 109 B |
BIN
public/Build/TemplateData/progress-bar-full-dark.png
Normal file
|
After Width: | Height: | Size: 74 B |
BIN
public/Build/TemplateData/progress-bar-full-light.png
Normal file
|
After Width: | Height: | Size: 84 B |
16
public/Build/TemplateData/style.css
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
body { padding: 0; margin: 0 }
|
||||
#unity-container { position: absolute }
|
||||
#unity-container.unity-desktop { left: 50%; top: 50%; transform: translate(-50%, -50%) }
|
||||
#unity-container.unity-mobile { position: fixed; width: 100%; height: 100% }
|
||||
#unity-canvas { background: #231F20 }
|
||||
.unity-mobile #unity-canvas { width: 100%; height: 100% }
|
||||
#unity-loading-bar { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); display: none }
|
||||
#unity-logo { width: 154px; height: 130px; background: url('unity-logo-dark.png') no-repeat center }
|
||||
#unity-progress-bar-empty { width: 141px; height: 18px; margin-top: 10px; margin-left: 6.5px; background: url('progress-bar-empty-dark.png') no-repeat center }
|
||||
#unity-progress-bar-full { width: 0%; height: 18px; margin-top: 10px; background: url('progress-bar-full-dark.png') no-repeat center }
|
||||
#unity-footer { position: relative }
|
||||
.unity-mobile #unity-footer { display: none }
|
||||
#unity-webgl-logo { float:left; width: 204px; height: 38px; background: url('webgl-logo.png') no-repeat center }
|
||||
#unity-build-title { float: right; margin-right: 10px; line-height: 38px; font-family: arial; font-size: 18px }
|
||||
#unity-fullscreen-button { cursor:pointer; float: right; width: 38px; height: 38px; background: url('fullscreen-button.png') no-repeat center }
|
||||
#unity-warning { position: absolute; left: 50%; top: 5%; transform: translate(-50%); background: white; padding: 10px; display: none }
|
||||
BIN
public/Build/TemplateData/unity-logo-dark.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
public/Build/TemplateData/unity-logo-light.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
public/Build/TemplateData/webgl-logo.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/Build/TemplateData/webmemd-icon.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
135
public/Build/index.html
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en-us">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>Unity WebGL Player | TetrisMP</title>
|
||||
<link rel="shortcut icon" href="TemplateData/favicon.ico">
|
||||
<link rel="stylesheet" href="TemplateData/style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="unity-container" class="unity-desktop">
|
||||
<canvas id="unity-canvas" width=1280 height=720 tabindex="-1"></canvas>
|
||||
<div id="unity-loading-bar">
|
||||
<div id="unity-logo"></div>
|
||||
<div id="unity-progress-bar-empty">
|
||||
<div id="unity-progress-bar-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="unity-warning"> </div>
|
||||
<div id="unity-footer">
|
||||
<div id="unity-webgl-logo"></div>
|
||||
<div id="unity-fullscreen-button"></div>
|
||||
<div id="unity-build-title">TetrisMP</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
|
||||
var container = document.querySelector("#unity-container");
|
||||
var canvas = document.querySelector("#unity-canvas");
|
||||
var loadingBar = document.querySelector("#unity-loading-bar");
|
||||
var progressBarFull = document.querySelector("#unity-progress-bar-full");
|
||||
var fullscreenButton = document.querySelector("#unity-fullscreen-button");
|
||||
var warningBanner = document.querySelector("#unity-warning");
|
||||
|
||||
// Shows a temporary message banner/ribbon for a few seconds, or
|
||||
// a permanent error message on top of the canvas if type=='error'.
|
||||
// If type=='warning', a yellow highlight color is used.
|
||||
// Modify or remove this function to customize the visually presented
|
||||
// way that non-critical warnings and error messages are presented to the
|
||||
// user.
|
||||
function unityShowBanner(msg, type) {
|
||||
function updateBannerVisibility() {
|
||||
warningBanner.style.display = warningBanner.children.length ? 'block' : 'none';
|
||||
}
|
||||
var div = document.createElement('div');
|
||||
div.innerHTML = msg;
|
||||
warningBanner.appendChild(div);
|
||||
if (type == 'error') div.style = 'background: red; padding: 10px;';
|
||||
else {
|
||||
if (type == 'warning') div.style = 'background: yellow; padding: 10px;';
|
||||
setTimeout(function () {
|
||||
warningBanner.removeChild(div);
|
||||
updateBannerVisibility();
|
||||
}, 5000);
|
||||
}
|
||||
updateBannerVisibility();
|
||||
}
|
||||
|
||||
var buildUrl = "Build";
|
||||
var loaderUrl = buildUrl + "/prod.loader.js";
|
||||
var config = {
|
||||
dataUrl: buildUrl + "/prod.data",
|
||||
frameworkUrl: buildUrl + "/prod.framework.js",
|
||||
codeUrl: buildUrl + "/prod.wasm",
|
||||
streamingAssetsUrl: "StreamingAssets",
|
||||
companyName: "DefaultCompany",
|
||||
productName: "TetrisMP",
|
||||
productVersion: "1.0",
|
||||
showBanner: unityShowBanner,
|
||||
};
|
||||
|
||||
/*UNITY BRIDGE*/
|
||||
function sendMessageToReact(message) {
|
||||
if (window.parent) {
|
||||
window.parent.postMessage(message, "*"); // Replace "*" with your React app's origin for security
|
||||
} else {
|
||||
console.error("Parent window not found.");
|
||||
}
|
||||
}
|
||||
// By default, Unity keeps WebGL canvas render target size matched with
|
||||
// the DOM size of the canvas element (scaled by window.devicePixelRatio)
|
||||
// Set this to false if you want to decouple this synchronization from
|
||||
// happening inside the engine, and you would instead like to size up
|
||||
// the canvas DOM size and WebGL render target sizes yourself.
|
||||
// config.matchWebGLToCanvasSize = false;
|
||||
|
||||
if (/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) {
|
||||
// Mobile device style: set canvas to 80% of screen size to account for iframe padding
|
||||
|
||||
var meta = document.createElement('meta');
|
||||
meta.name = 'viewport';
|
||||
meta.content = 'width=device-width, height=device-height, initial-scale=1.0, user-scalable=no, shrink-to-fit=yes';
|
||||
document.getElementsByTagName('head')[0].appendChild(meta);
|
||||
container.className = "unity-mobile";
|
||||
canvas.className = "unity-mobile";
|
||||
|
||||
// Set canvas size to 80% of viewport dimensions
|
||||
canvas.style.height = "80vh";
|
||||
|
||||
// To lower canvas resolution on mobile devices to gain some
|
||||
// performance, uncomment the following line:
|
||||
// config.devicePixelRatio = 1;
|
||||
|
||||
} else {
|
||||
// Desktop style: Render the game canvas in a window that can be maximized to fullscreen:
|
||||
|
||||
canvas.style.width = "1280px";
|
||||
canvas.style.height = "720px";
|
||||
}
|
||||
|
||||
loadingBar.style.display = "block";
|
||||
|
||||
var script = document.createElement("script");
|
||||
script.src = loaderUrl;
|
||||
script.onload = () => {
|
||||
createUnityInstance(canvas, config, (progress) => {
|
||||
progressBarFull.style.width = 100 * progress + "%";
|
||||
}).then((unityInstance) => {
|
||||
loadingBar.style.display = "none";
|
||||
fullscreenButton.onclick = () => {
|
||||
unityInstance.SetFullscreen(1);
|
||||
};
|
||||
}).catch((message) => {
|
||||
alert(message);
|
||||
});
|
||||
};
|
||||
|
||||
document.body.appendChild(script);
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
BIN
public/dino_icon.jpg
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
68
public/game.html
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dino Game - Unity Export</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #87CEEB;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
.game-container {
|
||||
text-align: center;
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.dino-placeholder {
|
||||
font-size: 8rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: #2563eb;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="game-container">
|
||||
<div class="dino-placeholder">🦖</div>
|
||||
<h1>Dino Game</h1>
|
||||
<div class="instructions">
|
||||
<p>This is a placeholder for your Unity game export.</p>
|
||||
<p>To integrate your actual game:</p>
|
||||
<ol style="text-align: left; display: inline-block;">
|
||||
<li>Export your Unity game to WebGL</li>
|
||||
<li>Replace this file with your game's <span class="highlight">index.html</span></li>
|
||||
<li>Ensure your game works in an iframe</li>
|
||||
<li>Test the integration</li>
|
||||
</ol>
|
||||
<p style="margin-top: 1rem;">
|
||||
<strong>Current Status:</strong>
|
||||
<span style="color: #059669;">✅ Payment Confirmed</span>
|
||||
</p>
|
||||
<p style="margin-top: 1rem;">
|
||||
<strong>Game Ready:</strong>
|
||||
<span style="color: #dc2626;">⏳ Waiting for Unity Export</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
34
tailwind.config.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import type { Config } from 'tailwindcss'
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: 'var(--background)',
|
||||
foreground: 'var(--foreground)',
|
||||
},
|
||||
animation: {
|
||||
'fade-in': 'fadeIn 0.3s ease-in-out',
|
||||
'slide-in-from-top': 'slideInFromTop 0.3s ease-out',
|
||||
},
|
||||
keyframes: {
|
||||
fadeIn: {
|
||||
'0%': { opacity: '0' },
|
||||
'100%': { opacity: '1' },
|
||||
},
|
||||
slideInFromTop: {
|
||||
'0%': { opacity: '0', transform: 'translateY(-10px)' },
|
||||
'100%': { opacity: '1', transform: 'translateY(0)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
export default config
|
||||