feat: Add configurable draw cycles, improve UX
Backend: - Add configurable draw cycle settings (minutes/hourly/daily/weekly/custom) - Add CYCLE_TYPE, CYCLE_INTERVAL_*, CYCLE_DAILY_TIME, CYCLE_WEEKLY_* env vars - Add SALES_CLOSE_BEFORE_DRAW_MINUTES and CYCLES_TO_GENERATE_AHEAD - Fix SQLite parameter issue in scheduler Frontend: - Add 'Save This Link' section with copy button on ticket status page - Improve draw animation to show immediately when draw starts - Show 'Waiting for next round...' instead of 'Drawing Now!' after draw - Hide Buy Tickets button when waiting for next round - Skip draw animation if no tickets were sold - Keep winner screen open longer (15s) for next cycle to load - Auto-refresh to next lottery cycle after draw Telegram Bot: - Various improvements and fixes
This commit is contained in:
@@ -3,10 +3,12 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
interface DrawAnimationProps {
|
||||
winnerName: string;
|
||||
winningTicket: number;
|
||||
potAmount: number;
|
||||
winnerName?: string;
|
||||
winningTicket?: number;
|
||||
potAmount?: number;
|
||||
onComplete: () => void;
|
||||
// If true, show "drawing" animation without winner info
|
||||
isDrawing?: boolean;
|
||||
}
|
||||
|
||||
export function DrawAnimation({
|
||||
@@ -14,11 +16,14 @@ export function DrawAnimation({
|
||||
winningTicket,
|
||||
potAmount,
|
||||
onComplete,
|
||||
isDrawing = false,
|
||||
}: DrawAnimationProps) {
|
||||
const [phase, setPhase] = useState<'spinning' | 'revealing' | 'winner' | 'done'>('spinning');
|
||||
const [phase, setPhase] = useState<'spinning' | 'revealing' | 'winner' | 'no-winner' | 'done'>('spinning');
|
||||
const [displayTicket, setDisplayTicket] = useState(0);
|
||||
const [showConfetti, setShowConfetti] = useState(false);
|
||||
|
||||
const hasWinner = winnerName !== undefined && winningTicket !== undefined && potAmount !== undefined;
|
||||
|
||||
// Generate random ticket numbers during spin
|
||||
useEffect(() => {
|
||||
if (phase !== 'spinning') return;
|
||||
@@ -27,20 +32,40 @@ export function DrawAnimation({
|
||||
setDisplayTicket(Math.floor(Math.random() * 999999999) + 1);
|
||||
}, 50);
|
||||
|
||||
// After 2.5 seconds, start revealing
|
||||
// After 2.5 seconds, start revealing (only if we have winner info)
|
||||
const revealTimeout = setTimeout(() => {
|
||||
setPhase('revealing');
|
||||
if (hasWinner) {
|
||||
setPhase('revealing');
|
||||
}
|
||||
// If no winner, keep spinning until we get data or timeout
|
||||
}, 2500);
|
||||
|
||||
return () => {
|
||||
clearInterval(spinInterval);
|
||||
clearTimeout(revealTimeout);
|
||||
};
|
||||
}, [phase]);
|
||||
}, [phase, hasWinner]);
|
||||
|
||||
// When winner info becomes available during spinning, transition to revealing
|
||||
useEffect(() => {
|
||||
if (phase === 'spinning' && hasWinner) {
|
||||
const timer = setTimeout(() => {
|
||||
setPhase('revealing');
|
||||
}, 500);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [phase, hasWinner]);
|
||||
|
||||
// If isDrawing becomes false and we have no winner, show no-winner phase
|
||||
useEffect(() => {
|
||||
if (!isDrawing && !hasWinner && phase === 'spinning') {
|
||||
setPhase('no-winner');
|
||||
}
|
||||
}, [isDrawing, hasWinner, phase]);
|
||||
|
||||
// Slow down and reveal actual number
|
||||
useEffect(() => {
|
||||
if (phase !== 'revealing') return;
|
||||
if (phase !== 'revealing' || !winningTicket) return;
|
||||
|
||||
let speed = 50;
|
||||
let iterations = 0;
|
||||
@@ -69,11 +94,24 @@ export function DrawAnimation({
|
||||
|
||||
setShowConfetti(true);
|
||||
|
||||
// Auto-dismiss after 6 seconds
|
||||
// Auto-dismiss after 15 seconds (give time to load next cycle)
|
||||
const dismissTimeout = setTimeout(() => {
|
||||
setPhase('done');
|
||||
onComplete();
|
||||
}, 6000);
|
||||
}, 15000);
|
||||
|
||||
return () => clearTimeout(dismissTimeout);
|
||||
}, [phase, onComplete]);
|
||||
|
||||
// Handle no-winner phase
|
||||
useEffect(() => {
|
||||
if (phase !== 'no-winner') return;
|
||||
|
||||
// Auto-dismiss after 3 seconds
|
||||
const dismissTimeout = setTimeout(() => {
|
||||
setPhase('done');
|
||||
onComplete();
|
||||
}, 3000);
|
||||
|
||||
return () => clearTimeout(dismissTimeout);
|
||||
}, [phase, onComplete]);
|
||||
@@ -88,7 +126,7 @@ export function DrawAnimation({
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/90 backdrop-blur-sm"
|
||||
onClick={phase === 'winner' ? handleDismiss : undefined}
|
||||
onClick={phase === 'winner' || phase === 'no-winner' ? handleDismiss : undefined}
|
||||
>
|
||||
{/* Confetti Effect */}
|
||||
{showConfetti && (
|
||||
@@ -130,7 +168,7 @@ export function DrawAnimation({
|
||||
)}
|
||||
|
||||
{/* Winner Phase */}
|
||||
{phase === 'winner' && (
|
||||
{phase === 'winner' && hasWinner && (
|
||||
<div className="animate-winner-reveal">
|
||||
<div className="text-4xl mb-4">🎉🏆🎉</div>
|
||||
<div className="text-3xl md:text-4xl font-bold text-yellow-400 mb-6">
|
||||
@@ -143,11 +181,32 @@ export function DrawAnimation({
|
||||
</div>
|
||||
<div className="text-gray-300 text-sm mb-1">Winning Ticket</div>
|
||||
<div className="text-2xl font-mono text-bitcoin-orange mb-4">
|
||||
#{winningTicket.toLocaleString()}
|
||||
#{winningTicket!.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-gray-300 text-sm mb-1">Prize</div>
|
||||
<div className="text-4xl md:text-5xl font-bold text-green-400">
|
||||
{potAmount.toLocaleString()} sats
|
||||
{potAmount!.toLocaleString()} sats
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 text-gray-400 text-sm animate-pulse">
|
||||
Click anywhere to continue
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No Winner Phase (no tickets sold) */}
|
||||
{phase === 'no-winner' && (
|
||||
<div className="animate-winner-reveal">
|
||||
<div className="text-4xl mb-4">😔</div>
|
||||
<div className="text-2xl md:text-3xl font-bold text-gray-400 mb-6">
|
||||
No Tickets This Round
|
||||
</div>
|
||||
<div className="bg-gray-900 rounded-2xl p-8 border-2 border-gray-600 shadow-2xl">
|
||||
<div className="text-gray-300 text-lg mb-4">
|
||||
No tickets were sold for this draw.
|
||||
</div>
|
||||
<div className="text-bitcoin-orange text-xl font-semibold">
|
||||
Next draw starting soon!
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 text-gray-400 text-sm animate-pulse">
|
||||
@@ -211,4 +270,3 @@ export function DrawAnimation({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user