/** * CoinRush Missions Page Component * Beautiful missions UI with daily/weekly missions, rewards, and animations */ const { useState, useEffect, useRef, useCallback } = React; // ============================================ // MISSION ICONS // ============================================ const MissionIcons = { battle: ( ), trophy: ( ), coins: ( ), fire: ( ), calendar: ( ), star: ( ), }; // Rarity colors const RarityColors = { common: { bg: 'rgba(156, 163, 175, 0.15)', border: 'rgba(156, 163, 175, 0.4)', text: '#9ca3af', glow: 'rgba(156, 163, 175, 0.3)' }, uncommon: { bg: 'rgba(34, 197, 94, 0.15)', border: 'rgba(34, 197, 94, 0.4)', text: '#22c55e', glow: 'rgba(34, 197, 94, 0.3)' }, rare: { bg: 'rgba(59, 130, 246, 0.15)', border: 'rgba(59, 130, 246, 0.4)', text: '#3b82f6', glow: 'rgba(59, 130, 246, 0.3)' }, epic: { bg: 'rgba(168, 85, 247, 0.15)', border: 'rgba(168, 85, 247, 0.4)', text: '#a855f7', glow: 'rgba(168, 85, 247, 0.4)' }, legendary: { bg: 'rgba(245, 158, 11, 0.15)', border: 'rgba(245, 158, 11, 0.5)', text: '#f59e0b', glow: 'rgba(245, 158, 11, 0.5)' }, }; // ============================================ // FLOATING REWARD ANIMATION // ============================================ function FloatingReward({ reward, onComplete }) { const [visible, setVisible] = useState(true); useEffect(() => { const timer = setTimeout(() => { setVisible(false); setTimeout(onComplete, 300); }, 2000); return () => clearTimeout(timer); }, [onComplete]); return (
{reward.coins > 0 && (
+{reward.coins} coins
)} {reward.xp > 0 && (
+{reward.xp} XP
)} {reward.lottery_tickets > 0 && (
🎟️ +{reward.lottery_tickets} Lottery Ticket{reward.lottery_tickets > 1 ? 's' : ''}
)} {reward.badge && (
🏆 Badge Unlocked!
)} {reward.bonus && (
{reward.bonus.message}
)}
); } // ============================================ // MISSION CARD COMPONENT // ============================================ function MissionCard({ mission, onClaim, isAnimating }) { const progress = Math.min(mission.progress || 0, mission.target); const progressPercent = Math.min(100, (progress / mission.target) * 100); const isCompleted = progress >= mission.target; const isClaimed = mission.claimed; const rarity = mission.rarity || 'common'; const colors = RarityColors[rarity]; const icon = MissionIcons[mission.icon] || MissionIcons.star; // Format target for display const formatTarget = (target, category) => { if (category === 'wager') { return Math.round(target / 100); // Convert hundredths to coins } return target; }; const formatProgress = (progress, category) => { if (category === 'wager') { return Math.round(progress / 100); } return progress; }; const displayProgress = formatProgress(progress, mission.category); const displayTarget = formatTarget(mission.target, mission.category); return (
{/* Rarity indicator */}
{rarity.charAt(0).toUpperCase() + rarity.slice(1)}
{/* Status badge */} {isClaimed &&
✓ Claimed
} {isCompleted && !isClaimed &&
Ready!
} {/* Mission header */}
{icon}

{mission.name}

{mission.description}

{/* Progress bar */}
{displayProgress} / {displayTarget} {Math.round(progressPercent)}%
{/* Rewards */}
{mission.coin_reward > 0 && (
coins +{mission.coin_reward}
)}
+{mission.xp_reward} XP
{mission.bonus_reward && (
BONUS {getBonusShortText(mission.bonus_reward)}
)}
); } function getBonusDescription(bonus) { switch (bonus.type) { case 'lottery_tickets': return `${bonus.amount} lottery ticket${bonus.amount > 1 ? 's' : ''} for the monthly draw`; case 'profile_badge': return `Unlock the "${bonus.badge_id}" profile badge`; case 'chat_emote': return `Unlock a new chat emote`; case 'xp_boost': return `${bonus.amount}% XP boost for ${bonus.duration / 60} minutes`; default: return 'Special bonus'; } } function getBonusShortText(bonus) { switch (bonus.type) { case 'lottery_tickets': return `🎟️ ${bonus.amount} Ticket${bonus.amount > 1 ? 's' : ''}`; case 'profile_badge': return `🏆 Badge`; case 'chat_emote': return `😎 Emote`; case 'xp_boost': return `⚡ ${bonus.amount}% XP`; default: return 'Bonus'; } } // ============================================ // COUNTDOWN TIMER // ============================================ function CountdownTimer({ seconds, label }) { const [remaining, setRemaining] = useState(seconds); useEffect(() => { setRemaining(seconds); }, [seconds]); useEffect(() => { const interval = setInterval(() => { setRemaining(r => Math.max(0, r - 1)); }, 1000); return () => clearInterval(interval); }, []); const hours = Math.floor(remaining / 3600); const minutes = Math.floor((remaining % 3600) / 60); const secs = remaining % 60; return (
{label}
{String(hours).padStart(2, '0')} : {String(minutes).padStart(2, '0')} : {String(secs).padStart(2, '0')}
); } // ============================================ // SUMMARY CARD // ============================================ function SummaryCard({ icon, label, value, color, subtext }) { return (
{icon}
{label} {value} {subtext && {subtext}}
); } // ============================================ // MAIN MISSIONS APP // ============================================ function MissionsApp() { const [missions, setMissions] = useState({ daily: [], weekly: [] }); const [activeTab, setActiveTab] = useState('daily'); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [claimingId, setClaimingId] = useState(null); const [floatingRewards, setFloatingRewards] = useState([]); const [dailyResetIn, setDailyResetIn] = useState(0); const [weeklyResetIn, setWeeklyResetIn] = useState(0); // Load missions const loadMissions = useCallback(async () => { try { const response = await fetch('/api/missions'); if (!response.ok) { if (response.status === 401) { setError('login'); return; } throw new Error('Failed to load missions'); } const data = await response.json(); setMissions({ daily: data.daily || [], weekly: data.weekly || [] }); setDailyResetIn(data.daily_reset_in || 0); setWeeklyResetIn(data.weekly_reset_in || 0); setError(null); } catch (err) { console.error('Error loading missions:', err); setError('Failed to load missions'); } finally { setLoading(false); } }, []); useEffect(() => { loadMissions(); }, [loadMissions]); // Claim reward const handleClaim = async (mission) => { if (claimingId) return; setClaimingId(mission.id); try { const response = await fetch(`/api/missions/${mission.id}/claim`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Failed to claim'); } const result = await response.json(); // Show floating reward const rewardId = Date.now(); setFloatingRewards(prev => [...prev, { id: rewardId, coins: result.coins_earned || 0, xp: result.xp_earned || 0, lottery_tickets: result.lottery_tickets_earned || 0, badge: result.badge_earned, bonus: result.bonus }]); // Update navbar balance if (window.CoinRushBalance && result.new_balance !== undefined) { window.CoinRushBalance.update(result.new_balance, (result.coins_earned || 0) * 100); } // Reload missions await loadMissions(); // Show level up notification if applicable if (result.level_up) { // Could trigger a level up animation here console.log('Level up to', result.new_level); } } catch (err) { console.error('Claim error:', err); alert(err.message); } finally { setClaimingId(null); } }; const removeFloatingReward = (id) => { setFloatingRewards(prev => prev.filter(r => r.id !== id)); }; // Calculate summary stats const activeMissions = activeTab === 'daily' ? missions.daily : missions.weekly; const completedCount = activeMissions.filter(m => m.claimed).length; const totalCoins = activeMissions.filter(m => !m.claimed).reduce((sum, m) => sum + (m.coin_reward || 0), 0); const totalXP = activeMissions.filter(m => !m.claimed).reduce((sum, m) => sum + (m.xp_reward || 0), 0); // Count unclaimed missions for badges const unclaimedDaily = missions.daily.filter(m => !m.claimed).length; const unclaimedWeekly = missions.weekly.filter(m => !m.claimed).length; const totalUnclaimed = unclaimedDaily + unclaimedWeekly; if (loading) { return (
Loading missions...
); } if (error === 'login') { return (

Login Required

Please log in to view and complete missions

); } return (
{/* Floating rewards */} {floatingRewards.map(reward => ( removeFloatingReward(reward.id)} /> ))} {/* Header */}

Daily Missions

Complete missions to earn coins, XP, and exclusive rewards!

{/* Tab navigation */}
{/* Reset timer */}
{/* Summary cards */}
} label="Progress" value={`${completedCount}/${activeMissions.length}`} color="#a855f7" subtext="missions completed" /> } label="Coins Available" value={totalCoins} color="#4fffb0" subtext="from unclaimed" /> } label="XP Available" value={totalXP} color="#f59e0b" subtext="from unclaimed" />
{/* Mission grid */}
{activeMissions.length === 0 ? (
No missions available
) : ( activeMissions.map(mission => ( )) )}
); } // ============================================ // MAIN PAGE WRAPPER (manages user state for navbar) // ============================================ function MissionsPage() { const [user, setUser] = useState(null); const [userLoading, setUserLoading] = useState(true); // Fetch user on mount const fetchUser = useCallback(async () => { try { const res = await fetch('/me', { credentials: 'include' }); if (res.ok) { const data = await res.json(); setUser(data); } else { setUser(null); } } catch (e) { console.error('Failed to fetch user:', e); setUser(null); } finally { setUserLoading(false); } }, []); useEffect(() => { fetchUser(); // Listen for login/logout events const handleUserUpdated = () => fetchUser(); window.addEventListener('userLoggedIn', handleUserUpdated); window.addEventListener('userLoggedOut', handleUserUpdated); return () => { window.removeEventListener('userLoggedIn', handleUserUpdated); window.removeEventListener('userLoggedOut', handleUserUpdated); }; }, [fetchUser]); const handleLogout = async () => { try { await fetch('/logout', { method: 'POST', credentials: 'include' }); setUser(null); window.dispatchEvent(new CustomEvent('userLoggedOut')); } catch (e) { console.error('Logout failed:', e); } }; return (
); } // ============================================ // MOUNT APPLICATION // ============================================ const root = ReactDOM.createRoot(document.getElementById('root')); root.render();