/** * CoinRush React Navbar Component * Matches the existing navbar.html design */ const { useState, useEffect, useRef } = React; // ============================================ // GLOBAL BALANCE SERVICE // ============================================ // This service provides real-time balance updates across all components // with floating +/- animations window.CoinRushBalance = window.CoinRushBalance || { listeners: [], pendingAnimation: null, // Queue for delayed animations (wins during spin) // Subscribe to balance updates subscribe(callback) { this.listeners.push(callback); return () => { this.listeners = this.listeners.filter(cb => cb !== callback); }; }, // Update balance with optional delta animation // newBalance: balance in cents, delta: change in cents (positive or negative) // showAnimation: if false, queues the animation for later (use for wins during spin) update(newBalance, delta = null, showAnimation = true) { // Dispatch to all listeners (always update the displayed balance) this.listeners.forEach(cb => cb(newBalance, delta)); // Dispatch global event for components not using subscription window.dispatchEvent(new CustomEvent('coinrush:balance', { detail: { balance: newBalance, delta } })); // Show or queue floating animation if (delta !== null && delta !== 0) { if (showAnimation) { this.showFloatingDelta(delta); } else { // Queue the animation for later (when game animation completes) this.pendingAnimation = delta; } } }, // Show the pending animation (call this when game animation completes) showPendingAnimation() { if (this.pendingAnimation !== null) { this.showFloatingDelta(this.pendingAnimation); this.pendingAnimation = null; } }, // Clear pending animation without showing it clearPendingAnimation() { this.pendingAnimation = null; }, // Show floating +/- animation near the balance display showFloatingDelta(deltaCents) { const balanceEl = document.getElementById('navbarBalance'); if (!balanceEl) return; const rect = balanceEl.getBoundingClientRect(); const isPositive = deltaCents > 0; const displayAmount = Math.abs(deltaCents) / 100; // Create floating element const floater = document.createElement('div'); floater.className = `balance-floater ${isPositive ? 'positive' : 'negative'}`; floater.textContent = `${isPositive ? '+' : '-'}${displayAmount.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; // Position near the balance floater.style.position = 'fixed'; floater.style.left = `${rect.left + rect.width / 2}px`; floater.style.top = `${rect.top}px`; floater.style.zIndex = '10000'; document.body.appendChild(floater); // Remove after animation setTimeout(() => floater.remove(), 1500); } }; // Global function for easy access window.updateCoinRushBalance = (newBalance, delta, showAnimation = true) => { window.CoinRushBalance.update(newBalance, delta, showAnimation); }; // Show pending animation (call after game animation completes) window.showPendingBalanceAnimation = () => { window.CoinRushBalance.showPendingAnimation(); }; // ============================================ // UTILITIES // ============================================ const colorFrom = (name) => { if (!name) return '#666'; let hash = 0; for (let i = 0; i < name.length; i++) { hash = ((hash << 5) - hash) + name.charCodeAt(i); } const hue = Math.abs(hash % 360); return `hsl(${hue}, 65%, 50%)`; }; const fmt = (cents) => ((cents || 0) / 100).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const getAvatarBorderClass = (level) => { if (level >= 50) return 'border-challenger'; if (level >= 40) return 'border-ruby'; if (level >= 30) return 'border-diamond'; if (level >= 20) return 'border-gold'; if (level >= 10) return 'border-silver'; return 'border-bronze'; }; // Get the PNG border image path based on level const getBorderImage = (level) => { if (level >= 50) return '/static/img/borders/Challengerborder.png'; if (level >= 40) return '/static/img/borders/Rubyborder.png'; if (level >= 30) return '/static/img/borders/Diamondborder.png'; if (level >= 20) return '/static/img/borders/Goldborder.png'; if (level >= 10) return '/static/img/borders/Silverborder.png'; return '/static/img/borders/Bronzeborder.png'; }; const getLevelClass = (level) => { if (level >= 50) return 'level-challenger'; if (level >= 40) return 'level-ruby'; if (level >= 30) return 'level-diamond'; if (level >= 20) return 'level-gold'; if (level >= 10) return 'level-silver'; return 'level-bronze'; }; // ============================================ // GAMES DROPDOWN // ============================================ function GamesDropdown({ activePage }) { const games = [ { href: '/coinflip-react', name: 'Coinflip', key: 'coinflip', color: '#fbbf24' }, { href: '/jackpot-react', name: 'Jackpot', key: 'jackpot', color: '#3b82f6' }, { href: '/premium-jackpot', name: 'Premium', key: 'premium-jackpot', color: '#a855f7', badge: 'VIP' }, { divider: true }, { href: '/barbarian', name: 'Knights Arena', key: 'barbarian', color: '#ef4444' }, { href: '/plinko', name: 'Plinko', key: 'plinko', color: '#ffd700' }, { href: '/slice-dice', name: "Slice 'n Dice", key: 'slice_dice', color: '#ec4899' }, ]; return (
{games.map((game, idx) => game.divider ? (
) : ( {game.name} {game.badge && {game.badge}} ) )}
); } // ============================================ // USER DROPDOWN // ============================================ function UserDropdown({ user, onLogout, onBalanceUpdate }) { const [isOpen, setIsOpen] = useState(false); const [activeBonus, setActiveBonus] = useState(null); const [bonusCountdown, setBonusCountdown] = useState(''); const [displayBalance, setDisplayBalance] = useState(user?.balance || 0); const dropdownRef = useRef(null); // Subscribe to global balance updates useEffect(() => { // Sync initial balance setDisplayBalance(user?.balance || 0); }, [user?.balance]); useEffect(() => { const unsubscribe = window.CoinRushBalance.subscribe((newBalance, delta) => { setDisplayBalance(newBalance); // Also call parent callback if provided if (onBalanceUpdate) onBalanceUpdate(newBalance); }); // Also listen for legacy balance events const handleLegacyUpdate = (e) => { if (e.detail?.balance !== undefined) { setDisplayBalance(e.detail.balance); } }; window.addEventListener('coinrush:balance', handleLegacyUpdate); return () => { unsubscribe(); window.removeEventListener('coinrush:balance', handleLegacyUpdate); }; }, [onBalanceUpdate]); const level = user?.level_info?.current_level || 1; const avatarClass = getAvatarBorderClass(level); const levelClass = getLevelClass(level); const borderImage = getBorderImage(level); const freeBattles = user?.free_battles_available || 0; // Level progress const levelInfo = user?.level_info || {}; const levelGoal = Math.max(0, (levelInfo.next_level_wagered || 0) - (levelInfo.current_level_wagered || 0)); const rawProgress = Math.max(0, levelInfo.progress_in_level || 0); const progressPct = levelGoal > 0 ? Math.min(100, (rawProgress / levelGoal) * 100) : 100; const progressLabel = levelGoal > 0 ? `${fmt(rawProgress)} / ${fmt(levelGoal)}` : `${fmt(rawProgress)}`; // Fetch active bonuses for flame indicator useEffect(() => { const fetchBonuses = async () => { try { const res = await fetch('/api/daily-reward/status', { credentials: 'include' }); if (res.ok) { const data = await res.json(); console.log('Daily reward status:', data); // Check if XP boost is active if (data.active_bonuses?.xp_boost > 0 && data.active_bonuses?.xp_boost_expires) { // xp_boost_expires is a date string (YYYY-MM-DD) - expires at END of that day (midnight next day) const expiresDate = data.active_bonuses.xp_boost_expires; const expiresMidnight = new Date(expiresDate + 'T23:59:59'); setActiveBonus({ type: 'xp_boost', value: data.active_bonuses.xp_boost, expires: expiresMidnight }); } else { setActiveBonus(null); } } } catch (err) { console.error('Failed to fetch bonus status:', err); } }; fetchBonuses(); // Refresh every 60 seconds const interval = setInterval(fetchBonuses, 60000); return () => clearInterval(interval); }, []); // Countdown timer for active bonus useEffect(() => { if (!activeBonus?.expires) { setBonusCountdown(''); return; } const updateCountdown = () => { const now = new Date(); const diff = activeBonus.expires - now; if (diff <= 0) { setActiveBonus(null); setBonusCountdown(''); return; } const hours = Math.floor(diff / (1000 * 60 * 60)); const mins = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); if (hours > 0) { setBonusCountdown(`${hours}h ${mins}m`); } else { const secs = Math.floor((diff % (1000 * 60)) / 1000); setBonusCountdown(`${mins}m ${secs}s`); } }; updateCountdown(); const interval = setInterval(updateCountdown, 1000); return () => clearInterval(interval); }, [activeBonus]); // Close on outside click useEffect(() => { const handleClickOutside = (e) => { if (dropdownRef.current && !dropdownRef.current.contains(e.target)) { setIsOpen(false); } }; document.addEventListener('click', handleClickOutside); return () => document.removeEventListener('click', handleClickOutside); }, []); // Flame SVG icon const FlameIcon = () => ( ); // Handle flame click to open daily rewards const handleFlameClick = (e) => { e.stopPropagation(); if (window.dailyRewardsModal?.open) { window.dailyRewardsModal.open(); } }; return (
{/* Active Bonus Flame Indicator */} {activeBonus && (
{bonusCountdown}
)}
setIsOpen(!isOpen)} >
{user.username.slice(0, 2).toUpperCase()}
{user.username}
{fmt(displayBalance)} {freeBattles > 0 && {freeBattles} FREE}
{progressLabel} coins
LVL {level}
{/* Dropdown Menu */}
{user.username.slice(0, 2).toUpperCase()}
{user.username}
{fmt(displayBalance)} coins
); } // ============================================ // AUTH BUTTONS (Logged Out) // ============================================ function AuthButtons() { return (
); } // ============================================ // MAIN NAVBAR COMPONENT // ============================================ function CoinRushNavbar({ user, isConnected, activePage = 'jackpot', onLogout }) { const [missionCount, setMissionCount] = useState(0); // Check sessionStorage SYNCHRONOUSLY before any state is set // This ensures consistent behavior across page navigations const shouldPulseInitially = typeof window !== 'undefined' && !sessionStorage.getItem('coinrush_mission_pulse_shown'); const [missionPulse, setMissionPulse] = useState(false); // Handle pulse on first mount only - use a ref to track if we've handled this const pulseHandledRef = useRef(false); useEffect(() => { // Only run once per component mount, and only if we should pulse if (pulseHandledRef.current) return; pulseHandledRef.current = true; if (shouldPulseInitially && user) { // This is the first page load after login - start pulsing sessionStorage.setItem('coinrush_mission_pulse_shown', 'true'); setMissionPulse(true); // Stop pulsing after 15 seconds const pulseTimer = setTimeout(() => { setMissionPulse(false); }, 15000); return () => clearTimeout(pulseTimer); } }, [user]); // Include user so we wait for auth to complete // Clear pulse flag when user logs out useEffect(() => { if (!user) { sessionStorage.removeItem('coinrush_mission_pulse_shown'); setMissionPulse(false); } }, [user]); // Fetch mission count for logged-in users useEffect(() => { if (!user) { setMissionCount(0); return; } const fetchMissionCount = async () => { try { const res = await fetch('/api/missions', { credentials: 'include' }); if (res.ok) { const data = await res.json(); const unclaimed = [...(data.daily || []), ...(data.weekly || [])] .filter(m => !m.claimed).length; setMissionCount(unclaimed); } } catch (e) { console.error('Failed to fetch mission count:', e); } }; fetchMissionCount(); // Refresh mission count every 30 seconds const interval = setInterval(fetchMissionCount, 30000); return () => { clearInterval(interval); }; }, [user]); // Determine navbar theme based on active page const getNavbarTheme = () => { if (activePage === 'slice_dice') return 'theme-neon-pink'; if (activePage === 'lottery') return 'theme-golden-lottery'; if (activePage === 'fame') return 'theme-emerald-fame'; return ''; }; return (
{/* Left Side */}
{/* Logo */} CR COINRUSH Crypto Casino {/* Live Badge */} {isConnected !== undefined && ( {isConnected ? 'Live lobby' : 'Connecting...'} )} {/* Games Dropdown */}
{/* Right Side */}
{/* Hall of Fame */} Fame {/* Missions */} 0 ? 'mission-pulse' : ''}`} title="Daily & Weekly Missions" > Missions {missionCount > 0 && ( {missionCount} )} {/* Lottery */} Lottery {/* Social */} Social {/* Daily Rewards */} {/* Rules */} {/* User Area */}
{user ? ( ) : ( )}
); } // Export for use window.CoinRushNavbar = CoinRushNavbar;