/**
* CoinRush Lottery - React Implementation
* Premium lottery with countdown, ticket purchases, and animated draws
* No emoji icons - all SVG
*/
// ============================================
// SVG ICONS
// ============================================
const Icons = {
trophy: (
),
ticket: (
),
clock: (
),
star: (
),
dollar: (
),
gift: (
),
cart: (
),
info: (
),
calendar: (
),
target: (
),
sparkles: (
),
check: (
),
x: (
),
crown: (
),
minus: (
),
plus: (
),
loader: (
),
users: (
),
heart: (
),
dice: (
),
percent: (
)
};
// ============================================
// LOTTERY RULES MODAL
// ============================================
function LotteryRulesModal({ onClose }) {
// Prevent body scroll when modal is open
React.useEffect(() => {
document.body.style.overflow = 'hidden';
return () => { document.body.style.overflow = ''; };
}, []);
return (
e.stopPropagation()}>
{Icons.x}
{Icons.heart}
Monthly Lottery Rules
{/* Community Message */}
The CoinRush Monthly Lottery is 100% funded by CoinRush .
We created this lottery as a way to give back to our amazing community
and thank you for being part of the CoinRush family. Every month, we set aside
a portion of our revenue to reward our loyal players with a chance to win big!
{/* How to Get Tickets */}
{Icons.ticket}
How to Get Tickets
Earn FREE tickets: For every $50 wagered on any CoinRush game,
you automatically receive 1 FREE lottery ticket.
Buy tickets: Purchase additional tickets for $10 each
to increase your chances of winning.
{/* How the Draw Works */}
{Icons.calendar}
Monthly Draw
The lottery draw happens on the 1st of every month at midnight UTC .
A single winning ticket is randomly selected from all tickets in the pool.
The more tickets you have, the higher your chance of winning!
{/* Prize Pool */}
{Icons.trophy}
Prize Pool
The prize pool consists of 5% of the monthly platform revenue
plus all ticket purchase proceeds.
One lucky winner takes the entire prize pool!
Winnings are instantly credited to your CoinRush balance.
{/* Win Chance */}
{Icons.target}
Your Win Chance
Win chance = (Your Tickets / Total Tickets in Pool) × 100%
Your current win chance is displayed on your ticket stats card.
The draw is provably fair and uses a cryptographically secure random selection.
Got it!
);
}
// ============================================
// UTILITY FUNCTIONS
// ============================================
function formatCurrency(cents) {
return (cents / 100).toFixed(2);
}
function formatWithCommas(num) {
const parts = num.toString().split('.');
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
return parts.join('.');
}
// ============================================
// COUNTDOWN TIMER COMPONENT
// ============================================
function CountdownTimer({ drawAt }) {
const [timeLeft, setTimeLeft] = React.useState({ days: 0, hours: 0, minutes: 0, seconds: 0 });
const [isDrawing, setIsDrawing] = React.useState(false);
React.useEffect(() => {
if (!drawAt) return;
const updateTimer = () => {
const now = new Date();
const drawTime = new Date(drawAt);
const diff = Math.max(0, drawTime - now);
if (diff <= 0) {
setIsDrawing(true);
setTimeLeft({ days: 0, hours: 0, minutes: 0, seconds: 0 });
return;
}
setIsDrawing(false);
setTimeLeft({
days: Math.floor(diff / (1000 * 60 * 60 * 24)),
hours: Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)),
minutes: Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)),
seconds: Math.floor((diff % (1000 * 60)) / 1000)
});
};
updateTimer();
const interval = setInterval(updateTimer, 1000);
return () => clearInterval(interval);
}, [drawAt]);
return (
{Icons.clock}
Next Draw
{isDrawing ? 'DRAWING' : 'OPEN'}
{String(timeLeft.days).padStart(2, '0')}
DAYS
:
{String(timeLeft.hours).padStart(2, '0')}
HRS
:
{String(timeLeft.minutes).padStart(2, '0')}
MIN
:
{String(timeLeft.seconds).padStart(2, '0')}
SEC
);
}
// ============================================
// PRIZE POOL COMPONENT
// ============================================
function PrizePool({ prizePool, totalTickets }) {
const formattedPrize = formatWithCommas(formatCurrency(prizePool));
return (
{Icons.trophy}
Prize Pool
$
{formattedPrize}
{Icons.ticket}
{formatWithCommas(totalTickets)} tickets
);
}
// ============================================
// USER STATS COMPONENT
// ============================================
function UserStats({ stats, totalTickets }) {
if (!stats) {
return (
{Icons.ticket}
Your Tickets
Log in to view your lottery tickets and stats
);
}
const winChance = stats.win_chance || 0;
return (
{Icons.ticket}
Your Tickets
{stats.total_tickets || 0}
Total Tickets
{Icons.gift}
{stats.wagered_tickets || 0}
Earned (Free)
{Icons.cart}
{stats.purchased_tickets || 0}
Purchased
);
}
// ============================================
// BUY TICKETS COMPONENT
// ============================================
function BuyTickets({ ticketPrice, onPurchase, userLoggedIn }) {
const [quantity, setQuantity] = React.useState(1);
const [loading, setLoading] = React.useState(false);
const totalCost = quantity * ticketPrice;
const pricePerTicket = formatCurrency(ticketPrice);
const handleQuantityChange = (delta) => {
setQuantity(prev => Math.max(1, Math.min(100, prev + delta)));
};
const handleInputChange = (e) => {
const val = parseInt(e.target.value) || 1;
setQuantity(Math.max(1, Math.min(100, val)));
};
const handleBuy = async () => {
if (!userLoggedIn) {
if (window.showAuthModal) window.showAuthModal('login');
return;
}
setLoading(true);
try {
await onPurchase(quantity);
setQuantity(1);
} finally {
setLoading(false);
}
};
return (
{Icons.cart}
Buy Tickets
${pricePerTicket} / ticket
Total
${formatCurrency(totalCost)}
{loading ? (
<>
{Icons.loader}
Processing...
>
) : (
<>
{Icons.ticket}
Purchase {quantity} Ticket{quantity > 1 ? 's' : ''}
>
)}
);
}
// ============================================
// WAGER PROGRESS COMPONENT
// ============================================
function WagerProgress({ stats, wagerPerTicket }) {
const targetDollars = formatCurrency(wagerPerTicket);
// If not logged in, show the card with 0 progress
const progressInCycle = stats ? (stats.wager_progress || 0) % wagerPerTicket : 0;
const percentage = (progressInCycle / wagerPerTicket) * 100;
const progressDollars = formatCurrency(progressInCycle);
return (
{Icons.star}
Earn Free Tickets
Wager ${targetDollars} on any game to earn 1 FREE lottery ticket!
${progressDollars}
/ ${targetDollars}
);
}
// ============================================
// HOW IT WORKS COMPONENT
// ============================================
function HowItWorks() {
const steps = [
{
icon: Icons.star,
iconClass: 'lt-icon-star',
title: 'Earn Free Tickets',
desc: 'For every $50 wagered on any game, you automatically earn 1 FREE lottery ticket.'
},
{
icon: Icons.cart,
iconClass: 'lt-icon-cart',
title: 'Buy Extra Tickets',
desc: 'Want more chances? Buy additional tickets for just $10 each to boost your odds!'
},
{
icon: Icons.calendar,
iconClass: 'lt-icon-calendar',
title: 'Monthly Draw',
desc: 'Drawings happen on the 1st of every month at midnight UTC. Watch the countdown!'
},
{
icon: Icons.trophy,
iconClass: 'lt-icon-trophy',
title: 'Win the Prize Pool',
desc: 'Prize pool is 5% of monthly revenue. One lucky ticket wins 100% of it!'
}
];
return (
{Icons.info}
How It Works
{steps.map((step, i) => (
{i + 1}
{step.icon}
{step.title}
{step.desc}
))}
);
}
// ============================================
// RECENT WINNERS COMPONENT
// ============================================
function RecentWinners({ winners }) {
return (
{Icons.crown}
Recent Winners
{winners.length === 0 ? (
{Icons.sparkles}
No draws yet. Be the first winner!
) : (
{winners.map((winner, i) => (
#{i + 1}
{winner.period}
{winner.winner_username || 'Anonymous'}
Won
${formatCurrency(winner.prize_pool)}
{Icons.ticket}
#{winner.winning_ticket_number}
))}
)}
);
}
// ============================================
// TOAST NOTIFICATION
// ============================================
function Toast({ message, type, onClose }) {
React.useEffect(() => {
const timer = setTimeout(onClose, 4000);
return () => clearTimeout(timer);
}, [onClose]);
const icon = type === 'success' ? Icons.check : type === 'error' ? Icons.x : Icons.info;
return (
{icon}
{message}
{Icons.x}
);
}
// ============================================
// MAIN LOTTERY APP
// ============================================
function LotteryApp() {
const [user, setUser] = React.useState(null);
const [loading, setLoading] = React.useState(true);
const [showRules, setShowRules] = React.useState(false);
const [lotteryState, setLotteryState] = React.useState({
status: 'open',
prizePool: 0,
totalTickets: 0,
ticketPrice: 1000,
wagerPerTicket: 5000,
drawAt: null,
userStats: null
});
const [winners, setWinners] = React.useState([]);
const [toast, setToast] = React.useState(null);
// Expose rules modal to navbar
React.useEffect(() => {
window.showLotteryRules = () => setShowRules(true);
return () => { delete window.showLotteryRules; };
}, []);
// Fetch user
const fetchUser = React.useCallback(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);
}
}, []);
// Fetch lottery state
const fetchLotteryState = React.useCallback(async () => {
try {
const res = await fetch('/api/lottery/state', { credentials: 'include' });
if (res.ok) {
const data = await res.json();
setLotteryState({
status: data.status,
prizePool: data.prize_pool,
totalTickets: data.total_tickets,
ticketPrice: data.ticket_price,
wagerPerTicket: data.wager_per_ticket,
drawAt: data.draw_at,
userStats: data.user_stats
});
}
} catch (e) {
console.error('Failed to fetch lottery state:', e);
}
}, []);
// Fetch winners history
const fetchHistory = React.useCallback(async () => {
try {
const res = await fetch('/api/lottery/history', { credentials: 'include' });
if (res.ok) {
const data = await res.json();
setWinners(data.draws || []);
}
} catch (e) {
console.error('Failed to fetch history:', e);
}
}, []);
// Initial load
React.useEffect(() => {
Promise.all([fetchUser(), fetchLotteryState(), fetchHistory()])
.finally(() => setLoading(false));
// Refresh state every 30 seconds
const interval = setInterval(fetchLotteryState, 30000);
return () => clearInterval(interval);
}, [fetchUser, fetchLotteryState, fetchHistory]);
// Buy tickets handler
const handlePurchase = async (quantity) => {
try {
const res = await fetch('/api/lottery/buy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ quantity })
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.detail || 'Failed to purchase tickets');
}
// Update state
setLotteryState(prev => ({
...prev,
prizePool: data.prize_pool,
totalTickets: data.total_tickets,
userStats: data.user_stats
}));
// Update navbar balance
if (window.updateBalanceDisplay && data.balance !== undefined) {
window.updateBalanceDisplay(data.balance);
}
// Refresh user
fetchUser();
setToast({
message: `Successfully purchased ${quantity} ticket${quantity > 1 ? 's' : ''}! 🎉`,
type: 'success'
});
} catch (error) {
setToast({ message: error.message, type: 'error' });
throw error;
}
};
if (loading) {
return (
{Icons.loader}
Loading lottery...
);
}
return (
{/* Floating Glowing Orbs Background */}
{/* Animated Floating Particles */}
{[...Array(35)].map((_, i) => (
))}
{/* Rules Modal */}
{showRules &&
setShowRules(false)} />}
{/* Toast Notifications */}
{toast && (
setToast(null)}
/>
)}
{/* Navbar */}
{ setUser(null); fetchLotteryState(); }}
/>
{/* Hero Section - Compact */}
Monthly Lottery
Get tickets, win big!
{/* Top Row - Prize, Countdown, Earn Free */}
{/* Main Content Grid - Stats & Buy */}
{/* How It Works */}
{/* Recent Winners */}
);
}
// ============================================
// RENDER APP
// ============================================
const lotteryRoot = ReactDOM.createRoot(document.getElementById('root'));
lotteryRoot.render( );