/** * CoinRush Premium Jackpot - React Version * Hourly high-stakes jackpot with modern UI * VERSION: 8 - Compact participants panel, thinner history bar */ console.log('Premium Jackpot v8 loaded - Compact layout'); const { useState, useEffect, useRef, useCallback } = React; // ============================================ // CONFIG // ============================================ const CONFIG = { minBet: 100, maxBet: 1000000, wsReconnectDelay: 3000, refreshInterval: 30000 }; // ============================================ // SVG ICONS // ============================================ const Icons = { trophy: ( ), coin: ( ), users: ( ), clock: ( ), spinner: ( ), diamond: ( ), star: ( ), zap: ( ), flame: ( ), gift: ( ), target: ( ), swords: ( ), slotMachine: ( ), lock: ( ), check: ( ), close: ( ), history: ( ), crown: ( ), plus: ( ), chevronDown: ( ), send: ( ), info: ( ), layers: ( ) }; // ============================================ // UTILITIES // ============================================ const fmt = (coins) => (coins || 0).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const fmtCents = (cents) => ((cents || 0) / 100).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const getColor = (username) => { if (!username) return '#3b82f6'; let hash = 0; for (let i = 0; i < username.length; i++) { hash = ((hash << 5) - hash) + username.charCodeAt(i); } const hue = Math.abs(hash % 360); const sat = 70 + (Math.abs(hash) % 30); const lit = 50 + (Math.abs(hash >> 8) % 20); return `hsl(${hue}, ${sat}%, ${lit}%)`; }; // ============================================ // DAILY REWARDS HELPERS // ============================================ const getRewardIcon = (type) => { const typeIcons = { 'xp_boost': Icons.zap, 'coinflip_discount': Icons.target, 'barbarian_discount': Icons.swords, 'jackpot_discount': Icons.slotMachine, 'combo': Icons.star }; return typeIcons[type] || Icons.gift; }; const getRewardColor = (type) => { const colors = { 'xp_boost': { primary: '#ffd700', secondary: '#ff9500', gradient: 'linear-gradient(135deg, #ffd700, #ff9500)' }, 'coinflip_discount': { primary: '#3b82f6', secondary: '#1d4ed8', gradient: 'linear-gradient(135deg, #3b82f6, #1d4ed8)' }, 'barbarian_discount': { primary: '#ef4444', secondary: '#dc2626', gradient: 'linear-gradient(135deg, #ef4444, #dc2626)' }, 'jackpot_discount': { primary: '#a855f7', secondary: '#7c3aed', gradient: 'linear-gradient(135deg, #a855f7, #7c3aed)' }, 'combo': { primary: '#4fffb0', secondary: '#2dd490', gradient: 'linear-gradient(135deg, #4fffb0, #2dd490)' } }; return colors[type] || { primary: '#4fffb0', secondary: '#2dd490', gradient: 'linear-gradient(135deg, #4fffb0, #2dd490)' }; }; // ============================================ // BONUS DATA (No emojis - use icons) // ============================================ const BONUS_DATA = { 'rush_hour': { title: 'Rush Hour', icon: 'zap', description: 'Winner gets DOUBLE prize + 1000 XP!', special: true }, 'diamond_rush': { title: 'Diamond Rush', icon: 'diamond', description: 'Winner gets +500 XP & Neon Crown', special: false }, 'golden_hour': { title: 'Golden Hour', icon: 'star', description: 'Winner gets +300 XP & Gold Badge', special: false }, 'lucky_streak': { title: 'Lucky Streak', icon: 'target', description: 'Winner gets +400 XP & Lucky Charm', special: false }, 'mega_boost': { title: 'Mega Boost', icon: 'zap', description: 'Winner gets +350 XP & Rocket Badge', special: false } }; // ============================================ // BET PANEL COMPONENT // ============================================ function BetPanel({ user, round, onDeposit, isLocked }) { const [amount, setAmount] = useState('100'); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); const [isPlacing, setIsPlacing] = useState(false); const presets = [100, 250, 500, 1000, 2500]; const handleQuickBet = (value) => { setAmount(String(value)); setError(''); }; const handleMaxBet = () => { if (!user) return; const maxBet = Math.floor(user.balance / 100); setAmount(String(maxBet)); setError(''); }; const handlePlaceBet = async () => { setError(''); setSuccess(''); if (!user) { window.openAuthModal?.('login'); return; } const betAmount = parseFloat(amount); if (isNaN(betAmount) || betAmount <= 0) { setError('Please enter a valid amount'); return; } if (betAmount < CONFIG.minBet) { setError(`Minimum bet is ${CONFIG.minBet} coins`); return; } const balanceCoins = user.balance / 100; if (betAmount > balanceCoins) { setError(`Insufficient balance. You have ${fmt(balanceCoins)} coins`); return; } if (isLocked) { setError('Betting is closed for this round'); return; } setIsPlacing(true); try { await onDeposit(betAmount); setSuccess(`Successfully bet ${fmt(betAmount)} coins!`); setAmount('100'); setTimeout(() => setSuccess(''), 5000); } catch (e) { setError(e.message || 'Failed to place bet'); } finally { setIsPlacing(false); } }; // Calculate user's current chance const userBets = round?.participants?.filter(p => p.username === user?.username) || []; const totalUserBet = userBets.reduce((sum, p) => sum + (p.amount || 0), 0); const totalPool = round?.total || 0; const userChance = totalPool > 0 ? (totalUserBet / totalPool * 100) : 0; return (
{ setAmount(e.target.value); setError(''); }} placeholder="Enter amount..." min={CONFIG.minBet} disabled={isPlacing || isLocked} />
{presets.map(preset => ( ))}
{error &&
{error}
} {success &&
{success}
}
Min: {CONFIG.minBet} coins Fee: 3.2%
); } // ============================================ // COUNTDOWN DISPLAY COMPONENT // ============================================ function CountdownDisplay({ round, winner, onTimerEnd }) { const [timeLeft, setTimeLeft] = useState({ minutes: '--', seconds: '--', total: 0 }); const [showWinner, setShowWinner] = useState(false); const timerEndedRef = useRef(false); useEffect(() => { // Reset timer ended flag when round changes timerEndedRef.current = false; const updateCountdown = () => { if (!round?.scheduled_at) { setTimeLeft({ minutes: '--', seconds: '--', total: 0 }); return; } const now = Date.now(); const scheduledAt = new Date(round.scheduled_at).getTime(); let totalSeconds = Math.floor((scheduledAt - now) / 1000); // If timer just hit 0 and we haven't notified yet if (totalSeconds <= 0 && !timerEndedRef.current) { timerEndedRef.current = true; onTimerEnd?.(); } // Cap at 59 minutes if (totalSeconds > 3540) totalSeconds = 3540; if (totalSeconds < 0) totalSeconds = 0; const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; setTimeLeft({ minutes: String(minutes).padStart(2, '0'), seconds: String(seconds).padStart(2, '0'), total: totalSeconds }); }; updateCountdown(); const interval = setInterval(updateCountdown, 1000); return () => clearInterval(interval); }, [round?.scheduled_at, round?.id, onTimerEnd]); // Show winner when there is one useEffect(() => { if (winner) { setShowWinner(true); } else { setShowWinner(false); } }, [winner]); const isUrgent = timeLeft.minutes === '00' && parseInt(timeLeft.seconds) <= 10; const isWarning = timeLeft.minutes === '00'; // Only show drawing if timer is 0 AND we don't have a winner yet const isDrawing = timeLeft.total === 0 && !winner && !showWinner; // WINNER MODE - Show for resolved rounds (HIGHEST PRIORITY) // This takes priority over everything else when winner exists if (winner) { return (
{Icons.crown}
WINNER
{winner.name}
WON {fmtCents(winner.prize)} coins
Next round starting soon...
{[...Array(30)].map((_, i) => (
))}
); } // DRAWING MODE if (isDrawing) { return (
Drawing Winner...
); } // NORMAL COUNTDOWN MODE return (
Next Draw In {round?.bonus_id && BONUS_DATA[round.bonus_id] && (
{BONUS_DATA[round.bonus_id]?.title || 'Bonus Active'}
)}
{timeLeft.minutes} min
:
{timeLeft.seconds} sec
{isUrgent ? 'Drawing very soon!' : isWarning ? 'Drawing soon!' : (round?.status === 'open' || round?.status === 'scheduled' || round?.status === 'countdown') ? 'Open for betting' : 'Preparing...'}
); } // ============================================ // JACKPOT HERO DISPLAY (Combined Prize + Timer) // Prize is the HERO, Timer builds tension // ============================================ function JackpotHeroDisplay({ round, winner, onTimerEnd, onWinnerDismiss }) { const [timeLeft, setTimeLeft] = useState({ minutes: '--', seconds: '--', total: 0 }); const timerEndedRef = useRef(false); // Auto-dismiss winner after 15 seconds useEffect(() => { if (winner && onWinnerDismiss) { const timer = setTimeout(onWinnerDismiss, 15000); return () => clearTimeout(timer); } }, [winner, onWinnerDismiss]); useEffect(() => { timerEndedRef.current = false; const updateCountdown = () => { if (!round?.scheduled_at) { setTimeLeft({ minutes: '--', seconds: '--', total: 0 }); return; } const now = Date.now(); const scheduledAt = new Date(round.scheduled_at).getTime(); let totalSeconds = Math.floor((scheduledAt - now) / 1000); if (totalSeconds <= 0 && !timerEndedRef.current) { timerEndedRef.current = true; onTimerEnd?.(); } if (totalSeconds > 3540) totalSeconds = 3540; if (totalSeconds < 0) totalSeconds = 0; const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; setTimeLeft({ minutes: String(minutes).padStart(2, '0'), seconds: String(seconds).padStart(2, '0'), total: totalSeconds }); }; updateCountdown(); const interval = setInterval(updateCountdown, 1000); return () => clearInterval(interval); }, [round?.scheduled_at, round?.id, onTimerEnd]); const total = round?.total || 0; const participantCount = round?.participants?.length || 0; const uniquePlayers = new Set(round?.participants?.map(p => p.username) || []).size; const isUrgent = timeLeft.minutes === '00' && parseInt(timeLeft.seconds) <= 10; const isWarning = timeLeft.minutes === '00'; const isDrawing = timeLeft.total === 0 && !winner; // Pre-compute confetti positions (memoized to avoid re-renders) const confettiPieces = React.useMemo(() => { const colors = ['#ffd700', '#ff6b6b', '#4fffb0', '#a855f7', '#3b82f6']; return [...Array(20)].map((_, i) => ({ left: `${(i * 5) + Math.random() * 5}%`, delay: `${(i * 0.15)}s`, color: colors[i % 5] })); }, []); // WINNER MODE - Show the winner's name prominently! if (winner) { console.log('๐Ÿ† JackpotHeroDisplay WINNER MODE:', winner); return (
{Icons.crown}
WINNER
{winner.name || 'Lucky Player'}
{fmtCents(winner.prize)} coins won!
{confettiPieces.map((piece, i) => (
))}
); } // DRAWING MODE if (isDrawing) { return (
Drawing Winner...
{fmtCents(total)}
); } // NORMAL MODE - Prize is the HERO return (
{/* Bonus Badge */} {round?.bonus_id && BONUS_DATA[round.bonus_id] && (
{BONUS_DATA[round.bonus_id]?.title || 'Bonus Active'}
)} {/* Main Prize - THE HERO */}
{Icons.trophy}
{fmtCents(total)} PRIZE POOL
{/* Timer - Secondary but tension-building */}
{timeLeft.minutes} : {timeLeft.seconds}
{isUrgent ? 'DRAWING NOW!' : isWarning ? 'FINAL MINUTE!' : 'until draw'}
{/* Stats Row */}
{Icons.users} {uniquePlayers} Players
{Icons.layers} {participantCount} Entries
{/* Status */}
{isUrgent ? 'Last chance to bet!' : isWarning ? 'Drawing soon!' : 'Open for betting'}
); } // ============================================ // PRIZE POOL DISPLAY (legacy, keeping for compatibility) // ============================================ function PrizePoolDisplay({ round }) { const total = round?.total || 0; const participantCount = round?.participants?.length || 0; // Calculate unique players const uniquePlayers = new Set(round?.participants?.map(p => p.username) || []).size; return (
{Icons.trophy}
{fmtCents(total)}
Total Prize Pool
{Icons.users} {uniquePlayers} Players
{Icons.layers} {participantCount} Entries
); } // ============================================ // SLIDING HISTORY BAR (Top) // ============================================ function SlidingHistoryBar({ history }) { if (!history || history.length === 0) return null; // Duplicate for seamless loop const items = [...history, ...history]; return (
{items.map((round, index) => { const color = getColor(round.winner); const date = new Date(round.scheduled_at); const timeStr = date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); return (
{round.winner?.charAt(0).toUpperCase() || '?'}
{round.winner || 'Unknown'} {fmtCents(round.winner_prize)} coins
{timeStr}
); })}
); } // ============================================ // COMPACT PARTICIPANTS PANEL (Left Sidebar) // ============================================ function CompactParticipantsPanel({ participants, currentUser }) { // Aggregate by username const aggregated = new Map(); (participants || []).forEach(p => { if (aggregated.has(p.username)) { const existing = aggregated.get(p.username); existing.amount += p.amount; existing.share_pct += p.share_pct; } else { aggregated.set(p.username, { ...p }); } }); const sorted = Array.from(aggregated.values()) .sort((a, b) => b.share_pct - a.share_pct); return (
{Icons.users} Participants {sorted.length}
{sorted.length === 0 ? (

No bets yet

Be the first to join!
) : (
{sorted.map((p) => { const isYou = currentUser?.username === p.username; const color = getColor(p.username); return (
{p.username.charAt(0).toUpperCase()}
{p.username} {isYou && You}
{p.amount.toFixed(2)} coins
{p.share_pct.toFixed(1)}%
); })}
)}
); } // ============================================ // BOTTOM PARTICIPANTS BAR (keeping for compatibility) // ============================================ function BottomParticipantsBar({ participants, currentUser }) { // Aggregate by username const aggregated = new Map(); (participants || []).forEach(p => { if (aggregated.has(p.username)) { const existing = aggregated.get(p.username); existing.amount += p.amount; existing.share_pct += p.share_pct; } else { aggregated.set(p.username, { ...p }); } }); const sorted = Array.from(aggregated.values()) .sort((a, b) => b.share_pct - a.share_pct); return (
{Icons.users} Active Participants
{sorted.length} players
{sorted.length === 0 ? (
No participants yet - be the first to join!
) : (
{sorted.map((p) => { const isYou = currentUser?.username === p.username; const color = getColor(p.username); return (
{p.username.charAt(0).toUpperCase()}
{p.username} {isYou && You}
{p.amount.toFixed(2)} coins
{p.share_pct.toFixed(1)}%
); })}
)}
); } // ============================================ // PARTICIPANTS LIST COMPONENT (keeping for compatibility) // ============================================ function ParticipantsList({ participants, currentUser }) { if (!participants || participants.length === 0) { return (
{Icons.users} Participants 0
{Icons.users}

No bets yet

Be the first to join!
); } // Aggregate by username const aggregated = new Map(); participants.forEach(p => { if (aggregated.has(p.username)) { const existing = aggregated.get(p.username); existing.amount += p.amount; existing.share_pct += p.share_pct; } else { aggregated.set(p.username, { ...p }); } }); const sorted = Array.from(aggregated.values()) .sort((a, b) => b.share_pct - a.share_pct); return (
{Icons.users} Participants {sorted.length}
{sorted.map((p, index) => { const isYou = currentUser?.username === p.username; const color = getColor(p.username); return (
#{index + 1}
{p.username.charAt(0).toUpperCase()}
{p.username} {isYou && You}
{p.amount.toFixed(2)} coins
{p.share_pct.toFixed(1)}%
); })}
); } // ============================================ // HISTORY PANEL COMPONENT // ============================================ function HistoryPanel({ history }) { if (!history || history.length === 0) { return (
{Icons.history} Recent Winners

No recent draws

); } const sorted = [...history].sort((a, b) => new Date(b.scheduled_at).getTime() - new Date(a.scheduled_at).getTime() ); return (
{Icons.history} Recent Winners
{sorted.slice(0, 10).map((round, index) => { const date = new Date(round.scheduled_at); const timeStr = date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); const dateStr = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); const color = getColor(round.winner); return (
{round.winner?.charAt(0).toUpperCase() || '?'}
{round.winner || 'Unknown'}
{dateStr} at {timeStr}
{fmtCents(round.winner_prize)} coins
); })}
); } // ============================================ // RULES MODAL // ============================================ function RulesModal({ isOpen, onClose }) { const [activeTab, setActiveTab] = useState('basics'); if (!isOpen) return null; const tabs = [ { id: 'basics', label: 'Basics', icon: Icons.info }, { id: 'how', label: 'How to Play', icon: Icons.target }, { id: 'prizes', label: 'Prizes', icon: Icons.trophy }, { id: 'bonuses', label: 'Bonuses', icon: Icons.star } ]; return (
e.stopPropagation()}>
{Icons.diamond}

Premium Jackpot Rules

Hourly high-stakes draws

{tabs.map(tab => ( ))}
{activeTab === 'basics' && (

What is Premium Jackpot?

Premium Jackpot is an hourly high-stakes lottery where players compete for a shared prize pool. Every hour on the hour, a winner is randomly selected based on their contribution to the pool.

  • Draws happen every hour at :00
  • Minimum bet: 100 coins
  • Higher bets = higher chance to win
  • House fee: 3.2%
)} {activeTab === 'how' && (

How to Play

  1. Place Your Bet - Enter the amount you want to bet (minimum 100 coins)
  2. Wait for the Draw - Watch the countdown timer
  3. Winner Selection - A random winner is picked at :00
  4. Collect Winnings - If you win, the prize is automatically credited

Your winning chance = Your Bet รท Total Pool ร— 100%

)} {activeTab === 'prizes' && (

Prize Distribution

  • Winner receives the entire prize pool minus 3.2% house fee
  • XP rewards based on participation and wins
  • Special bonuses during Rush Hour events
)} {activeTab === 'bonuses' && (

Hourly Bonuses

Each hour features a random bonus that enhances the winner's reward:

  • Rush Hour - DOUBLE prize + 1000 XP (rare!)
  • Diamond Rush - +500 XP & Neon Crown
  • Golden Hour - +300 XP & Gold Badge
  • Lucky Streak - +400 XP & Lucky Charm
  • Mega Boost - +350 XP & Rocket Badge
)}
); } // ============================================ // DAILY REWARD MODAL // ============================================ function DailyRewardModal({ isOpen, onClose, user, onClaim }) { const [status, setStatus] = useState(null); const [loading, setLoading] = useState(true); const [claiming, setClaiming] = useState(false); const [claimResult, setClaimResult] = useState(null); const [showConfetti, setShowConfetti] = useState(false); useEffect(() => { if (isOpen && user) { setLoading(true); setClaimResult(null); fetch('/api/daily-reward/status', { credentials: 'include' }) .then(res => res.ok ? res.json() : null) .then(data => { setStatus(data); setLoading(false); }) .catch(() => setLoading(false)); } }, [isOpen, user]); const handleClaim = async () => { if (claiming || !status || status.daily_reward_claimed) return; setClaiming(true); try { const res = await fetch('/api/daily-reward/claim', { method: 'POST', credentials: 'include' }); const data = await res.json(); if (res.ok) { setClaimResult(data); setShowConfetti(true); setStatus(prev => ({ ...prev, daily_reward_claimed: true })); onClaim?.(data); setTimeout(() => setShowConfetti(false), 3000); } else { setClaimResult({ error: data.detail || 'Failed to claim reward' }); } } catch (e) { setClaimResult({ error: 'Network error' }); } setClaiming(false); }; if (!isOpen) return null; const streak = status?.login_streak || 1; const reward = status?.today_reward || {}; const claimed = status?.daily_reward_claimed || false; const rewardColor = getRewardColor(reward.type); const allRewards = status?.all_rewards || {}; const visibleDays = []; const startDay = Math.max(1, streak - 3); const endDay = Math.min(30, startDay + 6); for (let i = startDay; i <= endDay; i++) { visibleDays.push({ day: i, reward: allRewards[i] || {}, isCurrent: i === streak, isPast: i < streak, isFuture: i > streak }); } const hasActiveBonuses = status?.active_bonuses && ( status.active_bonuses.xp_boost > 0 || status.active_bonuses.coinflip_discounts > 0 || status.active_bonuses.barbarian_discounts > 0 || status.active_bonuses.jackpot_discounts > 0 ); return (
e.stopPropagation()}> {showConfetti && (
{[...Array(60)].map((_, i) => (
))}
)}
{loading ? (
{Icons.spinner}
Loading rewards...
) : !user ? (
{Icons.lock}

Login Required

Please login to claim your daily rewards!

) : ( <>
{getRewardIcon(reward.type)}
{Icons.flame}
{streak} Day Streak

{claimed ? 'Reward Claimed!' : 'Daily Reward'}

{claimed ? 'Come back tomorrow to continue your streak!' : 'Claim your reward and keep your streak alive!' }

Day {streak}
{getRewardIcon(reward.type)}

{reward.name || 'Daily Reward'}

{reward.description || 'Claim your daily reward!'}

{claimed &&
{Icons.check}
}
Reward Calendar
{visibleDays.map(({ day, reward: dayReward, isCurrent, isPast, isFuture }) => { const dayColor = getRewardColor(dayReward.type); return (
Day {day}
{getRewardIcon(dayReward.type)}
{isPast &&
{Icons.check}
}
); })}
{hasActiveBonuses && (
Active Power-Ups
{status.active_bonuses.xp_boost > 0 && (
{Icons.zap} +{status.active_bonuses.xp_boost}% XP
)} {status.active_bonuses.coinflip_discounts > 0 && (
{Icons.target} {status.active_bonuses.coinflip_discounts}x Flip
)} {status.active_bonuses.barbarian_discounts > 0 && (
{Icons.swords} {status.active_bonuses.barbarian_discounts}x Duel
)} {status.active_bonuses.jackpot_discounts > 0 && (
{Icons.slotMachine} {status.active_bonuses.jackpot_discounts}x Jackpot
)}
)}
{claimed ? (
{Icons.clock} Next reward in Tomorrow
) : ( )} {claimResult?.error && (
{claimResult.error}
)}
)}
); } // ============================================ // WINNER CELEBRATION OVERLAY // ============================================ function WinnerCelebration({ winner, onClose }) { if (!winner) return null; useEffect(() => { const timer = setTimeout(onClose, 10000); return () => clearTimeout(timer); }, [onClose]); return (
e.stopPropagation()}>
{[...Array(50)].map((_, i) => (
))}
{Icons.crown}
WINNER!
{winner.name}
{fmtCents(winner.prize)} coins
); } // ============================================ // MAIN APP COMPONENT // ============================================ function PremiumJackpotApp() { const [user, setUser] = useState(null); const [round, setRound] = useState(null); const [history, setHistory] = useState([]); const [isConnected, setIsConnected] = useState(false); const [isLoading, setIsLoading] = useState(true); const [showRules, setShowRules] = useState(false); const [showDailyModal, setShowDailyModal] = useState(false); const [winner, setWinner] = useState(null); const [showAuthModal, setShowAuthModal] = useState(false); const [authMode, setAuthMode] = useState('login'); const wsRef = useRef(null); const winnerRef = useRef(null); const userRef = useRef(null); // Keep refs in sync useEffect(() => { winnerRef.current = winner; }, [winner]); useEffect(() => { userRef.current = user; }, [user]); // Expose functions to window for navbar useEffect(() => { window.showPremiumRules = () => setShowRules(true); window.dailyRewardsModal = { open: () => setShowDailyModal(true), close: () => setShowDailyModal(false) }; window.openAuthModal = (mode = 'login') => { setAuthMode(mode); setShowAuthModal(true); }; return () => { delete window.showPremiumRules; delete window.dailyRewardsModal; delete window.openAuthModal; }; }, []); // Fetch user on mount useEffect(() => { const fetchUser = async () => { try { const res = await fetch('/me', { credentials: 'include' }); if (res.ok) { const data = await res.json(); setUser(data); } } catch (e) { console.error('Failed to fetch user:', e); } }; fetchUser(); }, []); // Fetch round data const fetchRound = useCallback(async () => { try { const res = await fetch('/premium-jackpot/current', { credentials: 'include' }); if (res.ok) { const data = await res.json(); setRound(data); } } catch (e) { console.error('Failed to fetch round:', e); } finally { setIsLoading(false); } }, []); useEffect(() => { fetchRound(); const interval = setInterval(fetchRound, CONFIG.refreshInterval); return () => clearInterval(interval); }, [fetchRound]); // Fetch history useEffect(() => { const fetchHistory = async () => { try { const res = await fetch('/premium-jackpot/history', { credentials: 'include' }); if (res.ok) { const data = await res.json(); setHistory(data); } } catch (e) { console.error('Failed to fetch history:', e); } }; fetchHistory(); }, []); // WebSocket connection useEffect(() => { const connect = () => { const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'; const ws = new WebSocket(`${protocol}://${window.location.host}/ws/lobby`); ws.onopen = () => { console.log('๐Ÿ”— Premium Jackpot WS connected'); setIsConnected(true); }; ws.onerror = (error) => { console.error('โŒ Premium Jackpot WS error:', error); }; ws.onclose = () => { console.log('๐Ÿ”Œ Premium Jackpot WS disconnected'); setIsConnected(false); setTimeout(connect, CONFIG.wsReconnectDelay); }; ws.onmessage = (event) => { try { const data = JSON.parse(event.data); handleWSMessage(data); } catch (e) { console.error('WS message error:', e); } }; wsRef.current = ws; }; connect(); return () => { if (wsRef.current) { wsRef.current.close(); } }; }, []); const handleWSMessage = (data) => { console.log('๐Ÿ“ฅ Premium Jackpot WS:', data.type, data); switch (data.type) { case 'premium_jp_deposit': // Someone deposited - refresh round data fetchRound(); break; case 'premium_jp_winner': // Winner announced! Set winner state console.log('[PJ] Winner announced:', data.winner, 'Prize:', data.prize); setWinner({ name: data.winner, prize: data.prize }); // Refresh round and history after a delay (but winner display stays) setTimeout(() => { console.log('[PJ] Refreshing round data (winner still displayed)'); fetchRound(); }, 3000); setTimeout(() => { fetch('/premium-jackpot/history', { credentials: 'include' }) .then(r => r.ok ? r.json() : []) .then(setHistory); }, 4000); // Clear winner after 55 seconds so it shows until ~59:05 setTimeout(() => { console.log('[PJ] Clearing winner display after 55s'); setWinner(null); }, 55000); break; case 'premium_jp_new_round': // New round started - DON'T clear winner here, let the timeout handle it console.log('[PJ] New round started (keeping winner if exists)'); fetchRound(); break; case 'premium_jp_status': fetchRound(); break; case 'balance_update': // LIVE BALANCE UPDATE - use ref to get current user const currentUser = userRef.current; if (currentUser && data.username === currentUser.username) { const oldBalance = currentUser.balance || 0; const newBalance = data.balance_raw || data.balance; // Support both formats const delta = newBalance - oldBalance; console.log('[PJ] Balance update:', oldBalance, '->', newBalance, 'Delta:', delta); // Update local user state immediately setUser(prev => prev ? { ...prev, balance: newBalance } : prev); // Trigger navbar balance animation if (window.updateCoinRushBalance) { window.updateCoinRushBalance(newBalance, delta); } } break; } }; const handleDeposit = async (amount) => { const res = await fetch('/premium-jackpot/deposit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ amount: parseFloat(amount) }) }); if (!res.ok) { const error = await res.json().catch(() => ({ detail: 'Failed to place bet' })); throw new Error(error.detail || 'Failed to place bet'); } const data = await res.json(); setRound(data); // Update user balance with floating animation const newBalance = (user?.balance || 0) - amount * 100; setUser(prev => prev ? { ...prev, balance: newBalance } : prev); if (window.updateCoinRushBalance) { window.updateCoinRushBalance(newBalance, -amount * 100); } // Refresh user data fetch('/me', { credentials: 'include' }) .then(r => r.ok ? r.json() : null) .then(data => { if (data) setUser(data); }); }; const handleLogout = async () => { try { await fetch('/auth/logout', { method: 'POST', credentials: 'include' }); } finally { setUser(null); window.location.reload(); } }; // Only lock when resolved - scheduled, open, and countdown all allow betting const isLocked = round?.status === 'resolved'; return (
setShowRules(false)} /> setShowDailyModal(false)} user={user} onClaim={() => { fetch('/me', { credentials: 'include' }) .then(res => res.ok ? res.json() : null) .then(data => { if (data) setUser(data); }); }} /> {/* Auth Modal */} {typeof AuthModal !== 'undefined' && ( setShowAuthModal(false)} initialMode={authMode} onSuccess={(userData) => { setUser(userData); setShowAuthModal(false); }} /> )} {/* Winner is now shown inline in JackpotHeroDisplay, not as overlay */} {/* Sliding History Bar at Top */}
{isLoading ? (
{Icons.spinner}
Loading Premium Jackpot...
) : (
{/* Left Column - Bet Panel + Participants */} {/* Center - Main Display */}
{ console.log('[PJ] Timer ended, waiting for winner...'); }} onWinnerDismiss={() => setWinner(null)} />
{/* Right Column - Chat Only (stretched) */}
)}
); } // ============================================ // RENDER // ============================================ const root = ReactDOM.createRoot(document.getElementById('root')); root.render();