|
| 1 | +'use client'; |
| 2 | + |
| 3 | +import { useState, useEffect } from 'react'; |
| 4 | +import { motion } from 'framer-motion'; |
| 5 | +import { useWallet } from '@/contexts/WalletContext'; |
| 6 | +import { useHathor } from '@/contexts/HathorContext'; |
| 7 | +import { |
| 8 | + calculatePayout, |
| 9 | + formatTokenAmount, |
| 10 | + multiplierToThreshold, |
| 11 | + FORTUNE_TIGER_MULTIPLIERS, |
| 12 | +} from '@/lib/utils'; |
| 13 | +import { toast } from '@/lib/toast'; |
| 14 | +import { MultiplierSelector } from './MultiplierSelector'; |
| 15 | +import { SlotMachineAnimation } from './SlotMachineAnimation'; |
| 16 | +import { WinnerChickenDinner } from './WinnerChickenDinner'; |
| 17 | + |
| 18 | +interface FortuneTigerBetCardProps { |
| 19 | + selectedToken: string; |
| 20 | +} |
| 21 | + |
| 22 | +export default function FortuneTigerBetCard({ selectedToken }: FortuneTigerBetCardProps) { |
| 23 | + const { walletBalance, contractBalance, placeBet, connected, address, connectWallet, balance } = useWallet(); |
| 24 | + const { isConnected, getContractStateForToken, getContractIdForToken, allBets } = useHathor(); |
| 25 | + const contractBalanceInTokens = Number(contractBalance) / 100; |
| 26 | + const totalBalance = walletBalance + contractBalanceInTokens; |
| 27 | + |
| 28 | + const [betAmount, setBetAmount] = useState(10); |
| 29 | + const [selectedMultiplier, setSelectedMultiplier] = useState(2); |
| 30 | + const [threshold, setThreshold] = useState(32768); |
| 31 | + const [potentialPayout, setPotentialPayout] = useState(20); |
| 32 | + const [isPlacingBet, setIsPlacingBet] = useState(false); |
| 33 | + const [isSpinning, setIsSpinning] = useState(false); |
| 34 | + const [luckyNumber, setLuckyNumber] = useState(0); |
| 35 | + const [pendingBetTxId, setPendingBetTxId] = useState<string | null>(null); |
| 36 | + const [betResult, setBetResult] = useState<'win' | 'lose' | null>(null); |
| 37 | + const [showWinAnimation, setShowWinAnimation] = useState(false); |
| 38 | + const [showAuthPopup, setShowAuthPopup] = useState(false); |
| 39 | + |
| 40 | + const contractState = getContractStateForToken(selectedToken); |
| 41 | + const randomBitLength = contractState?.random_bit_length || 16; |
| 42 | + const houseEdgeBasisPoints = contractState?.house_edge_basis_points || 200; |
| 43 | + const maxBetAmount = contractState?.max_bet_amount || 1000000; |
| 44 | + |
| 45 | + // Calculate threshold and payout when multiplier or bet amount changes |
| 46 | + useEffect(() => { |
| 47 | + const newThreshold = multiplierToThreshold(selectedMultiplier, randomBitLength, houseEdgeBasisPoints); |
| 48 | + const payout = calculatePayout(betAmount, newThreshold, randomBitLength, houseEdgeBasisPoints); |
| 49 | + setThreshold(newThreshold); |
| 50 | + setPotentialPayout(payout); |
| 51 | + }, [betAmount, selectedMultiplier, randomBitLength, houseEdgeBasisPoints]); |
| 52 | + |
| 53 | + // Watch for bet results |
| 54 | + useEffect(() => { |
| 55 | + if (!pendingBetTxId) return; |
| 56 | + |
| 57 | + const bet = allBets.find(b => b.id === pendingBetTxId); |
| 58 | + if (bet && bet.result !== 'pending') { |
| 59 | + // Bet has been confirmed |
| 60 | + setLuckyNumber(bet.luckyNumber || 0); |
| 61 | + setIsSpinning(false); |
| 62 | + setBetResult(bet.result as 'win' | 'lose'); |
| 63 | + setPendingBetTxId(null); |
| 64 | + |
| 65 | + // Show winner animation if won |
| 66 | + if (bet.result === 'win') { |
| 67 | + setTimeout(() => { |
| 68 | + setShowWinAnimation(true); |
| 69 | + }, 500); // Small delay to let slot machine settle |
| 70 | + } |
| 71 | + } |
| 72 | + }, [allBets, pendingBetTxId]); |
| 73 | + |
| 74 | + const setQuickAmount = (percentage: number) => { |
| 75 | + const amount = totalBalance * percentage; |
| 76 | + const maxAllowed = Number(maxBetAmount) / 100; |
| 77 | + setBetAmount(Math.min(amount, maxAllowed)); |
| 78 | + }; |
| 79 | + |
| 80 | + const handleSpin = async () => { |
| 81 | + // If not connected, open wallet connection |
| 82 | + if (!connected) { |
| 83 | + connectWallet(); |
| 84 | + return; |
| 85 | + } |
| 86 | + |
| 87 | + // If connected but no balance available yet (need authorization) |
| 88 | + if (balance === 0n) { |
| 89 | + setShowAuthPopup(true); |
| 90 | + setTimeout(() => setShowAuthPopup(false), 3000); |
| 91 | + return; |
| 92 | + } |
| 93 | + |
| 94 | + if (betAmount <= 0) { |
| 95 | + toast.error('Bet amount must be positive'); |
| 96 | + return; |
| 97 | + } |
| 98 | + |
| 99 | + const maxAllowed = Number(maxBetAmount) / 100; |
| 100 | + if (betAmount > maxAllowed) { |
| 101 | + toast.error(`Bet amount exceeds maximum of ${formatTokenAmount(Number(maxBetAmount))} ${selectedToken}`); |
| 102 | + return; |
| 103 | + } |
| 104 | + |
| 105 | + if (betAmount > totalBalance) { |
| 106 | + toast.error('Insufficient balance'); |
| 107 | + return; |
| 108 | + } |
| 109 | + |
| 110 | + const contractId = getContractIdForToken(selectedToken); |
| 111 | + if (!contractId) { |
| 112 | + toast.error('Contract not found for token'); |
| 113 | + return; |
| 114 | + } |
| 115 | + |
| 116 | + const tokenUid = contractState?.token_uid || '00'; |
| 117 | + |
| 118 | + // Show wallet confirmation message |
| 119 | + toast.info('⏳ Please confirm the transaction in your wallet...'); |
| 120 | + |
| 121 | + setIsPlacingBet(true); |
| 122 | + setBetResult(null); |
| 123 | + |
| 124 | + try { |
| 125 | + const result = await placeBet(betAmount, threshold, selectedToken, contractId, tokenUid, contractBalance); |
| 126 | + setPendingBetTxId(result.response.hash); |
| 127 | + // Start spinning only after transaction is confirmed |
| 128 | + setIsSpinning(true); |
| 129 | + toast.success('🎰 Transaction confirmed! Spinning...'); |
| 130 | + } catch (error: any) { |
| 131 | + toast.error(error.message || 'Failed to place bet'); |
| 132 | + setIsSpinning(false); |
| 133 | + setBetResult(null); |
| 134 | + } finally { |
| 135 | + setIsPlacingBet(false); |
| 136 | + } |
| 137 | + }; |
| 138 | + |
| 139 | + return ( |
| 140 | + <> |
| 141 | + <div className="min-h-screen bg-gradient-to-br from-purple-900 via-pink-800 to-orange-700 flex items-center justify-center p-4"> |
| 142 | + <div className="w-full max-w-2xl relative"> |
| 143 | + {/* Wallet Address - Top Right */} |
| 144 | + {connected && address && ( |
| 145 | + <div className="absolute top-0 right-0 bg-white/20 backdrop-blur-sm rounded-xl px-4 py-2 border-2 border-white/30"> |
| 146 | + <div className="text-xs text-white/70 font-semibold">Wallet</div> |
| 147 | + <div className="text-sm text-white font-bold font-mono"> |
| 148 | + {address.slice(0, 6)}...{address.slice(-4)} |
| 149 | + </div> |
| 150 | + </div> |
| 151 | + )} |
| 152 | + |
| 153 | + {/* Header */} |
| 154 | + <div className="text-center mb-4 mt-16"> |
| 155 | + <motion.h2 |
| 156 | + initial={{ scale: 0.9 }} |
| 157 | + animate={{ scale: 1 }} |
| 158 | + className="text-4xl md:text-5xl font-black bg-gold-gradient bg-clip-text text-transparent mb-2" |
| 159 | + > |
| 160 | + 🐯 FORTUNE TIGER 🐯 |
| 161 | + </motion.h2> |
| 162 | + <p className="text-white text-base font-semibold">Pick your multiplier and spin to win!</p> |
| 163 | + </div> |
| 164 | + |
| 165 | + {/* Slot Machine Animation - Always Shown */} |
| 166 | + <div className="mb-4"> |
| 167 | + <SlotMachineAnimation |
| 168 | + isSpinning={isSpinning} |
| 169 | + finalNumber={luckyNumber} |
| 170 | + result={betResult} |
| 171 | + /> |
| 172 | + </div> |
| 173 | + |
| 174 | + {/* Quick Bet Amount Buttons */} |
| 175 | + <div className="mb-4"> |
| 176 | + <div className="grid grid-cols-4 gap-2"> |
| 177 | + {[1, 10, 100, 250].map(amount => ( |
| 178 | + <button |
| 179 | + key={amount} |
| 180 | + onClick={() => setBetAmount(amount)} |
| 181 | + disabled={isPlacingBet || isSpinning} |
| 182 | + className={`py-3 rounded-xl text-base font-bold transition-all disabled:opacity-50 disabled:cursor-not-allowed border-2 ${ |
| 183 | + betAmount === amount |
| 184 | + ? 'bg-yellow-400 text-orange-900 border-yellow-300' |
| 185 | + : 'bg-white/20 hover:bg-white/30 text-white border-white/20 hover:border-white/40' |
| 186 | + }`} |
| 187 | + > |
| 188 | + {amount} {selectedToken} |
| 189 | + </button> |
| 190 | + ))} |
| 191 | + </div> |
| 192 | + </div> |
| 193 | + |
| 194 | + {/* Multiplier Selector */} |
| 195 | + <div className="mb-4"> |
| 196 | + <MultiplierSelector |
| 197 | + selectedMultiplier={selectedMultiplier} |
| 198 | + onSelect={setSelectedMultiplier} |
| 199 | + disabled={isPlacingBet || isSpinning} |
| 200 | + /> |
| 201 | + </div> |
| 202 | + |
| 203 | + {/* Payout Display */} |
| 204 | + <motion.div |
| 205 | + animate={{ scale: isSpinning ? [1, 1.05, 1] : 1 }} |
| 206 | + transition={{ repeat: isSpinning ? Infinity : 0, duration: 1 }} |
| 207 | + className="bg-gradient-to-r from-yellow-400 via-orange-400 to-yellow-400 rounded-xl p-4 mb-4 text-center shadow-xl" |
| 208 | + > |
| 209 | + <div className="text-sm text-orange-900 font-bold">Potential Win</div> |
| 210 | + <div className="text-3xl md:text-4xl font-black text-orange-900"> |
| 211 | + {formatTokenAmount(potentialPayout * 100)} {selectedToken} |
| 212 | + </div> |
| 213 | + <div className="text-xs text-orange-900 font-bold"> |
| 214 | + {selectedMultiplier}x |
| 215 | + </div> |
| 216 | + </motion.div> |
| 217 | + |
| 218 | + {/* Spin Button */} |
| 219 | + <motion.button |
| 220 | + onClick={handleSpin} |
| 221 | + disabled={isPlacingBet || isSpinning} |
| 222 | + whileHover={!isPlacingBet && !isSpinning ? { scale: 1.05 } : {}} |
| 223 | + whileTap={!isPlacingBet && !isSpinning ? { scale: 0.95 } : {}} |
| 224 | + className="w-full bg-gradient-to-r from-green-500 to-emerald-600 text-white py-4 px-6 rounded-xl font-black text-2xl shadow-xl hover:shadow-green-500/50 transition-all disabled:opacity-50 disabled:cursor-not-allowed border-3 border-white/20" |
| 225 | + > |
| 226 | + {isSpinning ? '🎰 SPINNING...' : isPlacingBet ? '⏳ CONFIRMING...' : !connected ? '🔌 CONNECT WALLET' : '🎰 SPIN TO WIN!'} |
| 227 | + </motion.button> |
| 228 | + |
| 229 | + {/* Balance Info */} |
| 230 | + <div className="mt-3 text-center text-sm text-white/80 font-semibold"> |
| 231 | + <div>Balance: {formatTokenAmount(BigInt(Math.floor(totalBalance * 100)))} {selectedToken}</div> |
| 232 | + <div className="text-xs mt-1 text-white/60"> |
| 233 | + Wallet: {formatTokenAmount(BigInt(Math.floor(walletBalance * 100)))} | Contract: {formatTokenAmount(contractBalance)} |
| 234 | + </div> |
| 235 | + </div> |
| 236 | + </div> |
| 237 | + </div> |
| 238 | + |
| 239 | + {/* Winner Chicken Dinner Animation */} |
| 240 | + {showWinAnimation && ( |
| 241 | + <WinnerChickenDinner |
| 242 | + payout={potentialPayout} |
| 243 | + token={selectedToken} |
| 244 | + onComplete={() => setShowWinAnimation(false)} |
| 245 | + /> |
| 246 | + )} |
| 247 | + |
| 248 | + {/* Authorization Required Popup */} |
| 249 | + {showAuthPopup && ( |
| 250 | + <motion.div |
| 251 | + initial={{ opacity: 0 }} |
| 252 | + animate={{ opacity: 1 }} |
| 253 | + exit={{ opacity: 0 }} |
| 254 | + className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" |
| 255 | + > |
| 256 | + <motion.div |
| 257 | + initial={{ scale: 0.8, y: 20 }} |
| 258 | + animate={{ scale: 1, y: 0 }} |
| 259 | + className="bg-gradient-to-br from-orange-500 to-red-600 rounded-2xl p-8 max-w-md mx-4 border-4 border-yellow-300 shadow-2xl" |
| 260 | + > |
| 261 | + <div className="text-center"> |
| 262 | + <div className="text-6xl mb-4">🔐</div> |
| 263 | + <h3 className="text-2xl font-black text-white mb-3">Authorization Required</h3> |
| 264 | + <p className="text-white/90 text-lg"> |
| 265 | + Please authorize the action in your wallet to view your balance and start playing! |
| 266 | + </p> |
| 267 | + </div> |
| 268 | + </motion.div> |
| 269 | + </motion.div> |
| 270 | + )} |
| 271 | + </> |
| 272 | + ); |
| 273 | +} |
0 commit comments