/**
* 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 (
);
}
// ============================================
// 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 && (
)}
setIsOpen(!isOpen)}
>
{user.username.slice(0, 2).toUpperCase()}
{user.username}
{fmt(displayBalance)}
{freeBattles > 0 &&
{freeBattles} FREE}
{progressLabel} coins
{/* 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 */}
);
}
// Export for use
window.CoinRushNavbar = CoinRushNavbar;