Skip to content

Commit ccaa459

Browse files
committed
Add Tiger Mode
1 parent 0a96d0a commit ccaa459

12 files changed

+925
-1
lines changed

app/page.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import RecentOperationsTable from '@/components/RecentOperationsTable';
1010
import ProfitLossCard from '@/components/ProfitLossCard';
1111
import TokenSelector from '@/components/TokenSelector';
1212
import PlaceBetCard from '@/components/PlaceBetCard';
13+
import FortuneTigerBetCard from '@/components/FortuneTigerBetCard';
1314
import AddLiquidityCard from '@/components/AddLiquidityCard';
1415
import RemoveLiquidityCard from '@/components/RemoveLiquidityCard';
1516
import { ContractInfoCompact } from '@/components/ContractInfoCompact';
1617
import { NetworkSelector } from '@/components/NetworkSelector';
18+
import { UIModeSwitcher, type UIMode } from '@/components/UIModeSwitcher';
1719
import HelpIcon from '@/components/HelpIcon';
1820
import { formatBalance } from '@/lib/utils';
1921
import { toast } from '@/lib/toast';
@@ -31,6 +33,20 @@ export default function Home() {
3133
const [isRefreshingContract, setIsRefreshingContract] = useState(false);
3234
const [showWithdrawModal, setShowWithdrawModal] = useState(false);
3335
const [withdrawAmount, setWithdrawAmount] = useState('');
36+
const [uiMode, setUIMode] = useState<UIMode>('classic');
37+
38+
// Load UI mode preference from localStorage
39+
useEffect(() => {
40+
const storedMode = localStorage.getItem('ui_mode') as UIMode | null;
41+
if (storedMode && (storedMode === 'classic' || storedMode === 'fortune-tiger')) {
42+
setUIMode(storedMode);
43+
}
44+
}, []);
45+
46+
const handleModeChange = (mode: UIMode) => {
47+
setUIMode(mode);
48+
localStorage.setItem('ui_mode', mode);
49+
};
3450

3551
const handleCardToggle = (cardId: string) => {
3652
setExpandedCard(expandedCard === cardId ? null : cardId);
@@ -160,6 +176,16 @@ export default function Home() {
160176

161177
const contractState = getContractStateForToken(selectedToken);
162178

179+
// If in Fortune Tiger mode, render full-screen slot machine
180+
if (uiMode === 'fortune-tiger') {
181+
return (
182+
<div className="relative">
183+
<FortuneTigerBetCard selectedToken={selectedToken} />
184+
<UIModeSwitcher currentMode={uiMode} onModeChange={handleModeChange} />
185+
</div>
186+
);
187+
}
188+
163189
return (
164190
<div className="min-h-screen bg-slate-900">
165191
<Header />
@@ -359,6 +385,9 @@ export default function Home() {
359385
<footer className="mt-8 text-center text-slate-500 text-sm">
360386
<p>HathorDice {APP_VERSION}</p>
361387
</footer>
388+
389+
{/* UI Mode Switcher - Only show in classic mode */}
390+
<UIModeSwitcher currentMode={uiMode} onModeChange={handleModeChange} />
362391
</div>
363392
);
364393
}

components/FortuneTigerBetCard.tsx

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
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+
}

components/MultiplierSelector.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
'use client';
2+
3+
import { motion } from 'framer-motion';
4+
import { FORTUNE_TIGER_MULTIPLIERS, type FortuneTigerMultiplier } from '@/lib/utils';
5+
6+
interface MultiplierSelectorProps {
7+
selectedMultiplier: number;
8+
onSelect: (multiplier: number) => void;
9+
disabled?: boolean;
10+
}
11+
12+
const colorClasses = {
13+
green: 'bg-tiger-green hover:bg-emerald-600',
14+
blue: 'bg-blue-600 hover:bg-blue-700',
15+
purple: 'bg-purple-600 hover:bg-purple-700',
16+
orange: 'bg-orange-600 hover:bg-orange-700',
17+
red: 'bg-tiger-red hover:bg-tiger-red-dark',
18+
};
19+
20+
export function MultiplierSelector({ selectedMultiplier, onSelect, disabled }: MultiplierSelectorProps) {
21+
return (
22+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-3">
23+
{FORTUNE_TIGER_MULTIPLIERS.map((option: FortuneTigerMultiplier) => {
24+
const isSelected = selectedMultiplier === option.multiplier;
25+
const colorClass = colorClasses[option.color as keyof typeof colorClasses] || colorClasses.blue;
26+
27+
return (
28+
<motion.button
29+
key={option.multiplier}
30+
onClick={() => !disabled && onSelect(option.multiplier)}
31+
disabled={disabled}
32+
whileHover={!disabled ? { scale: 1.05 } : {}}
33+
whileTap={!disabled ? { scale: 0.95 } : {}}
34+
className={`
35+
relative p-4 rounded-lg font-bold text-white
36+
transition-all duration-200
37+
${isSelected ? 'ring-4 ring-tiger-gold animate-pulse-glow' : ''}
38+
${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
39+
${colorClass}
40+
`}
41+
>
42+
<div className="flex flex-col items-center gap-1">
43+
<span className="text-2xl md:text-3xl">
44+
{option.label}
45+
</span>
46+
<span className="text-xs md:text-sm opacity-90">
47+
{option.winChance.toFixed(1)}% win
48+
</span>
49+
</div>
50+
{isSelected && (
51+
<motion.div
52+
initial={{ scale: 0 }}
53+
animate={{ scale: 1 }}
54+
className="absolute -top-2 -right-2 bg-tiger-gold text-slate-900 rounded-full w-6 h-6 flex items-center justify-center text-xs font-bold"
55+
>
56+
57+
</motion.div>
58+
)}
59+
</motion.button>
60+
);
61+
})}
62+
</div>
63+
);
64+
}

0 commit comments

Comments
 (0)