/**
* 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}
)}
{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 && (
+{mission.coin_reward}
)}
{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 (
);
}
if (error === 'login') {
return (
Login Required
Please log in to view and complete missions
);
}
return (
{/* Floating rewards */}
{floatingRewards.map(reward => (
removeFloatingReward(reward.id)}
/>
))}
{/* Header */}
{/* 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();