Initial commit: Lightning Lottery - Bitcoin Lightning Network powered lottery
Features: - Lightning Network payments via LNbits integration - Provably fair draws using CSPRNG - Random ticket number generation - Automatic payouts with retry/redraw logic - Nostr authentication (NIP-07) - Multiple draw cycles (hourly, daily, weekly, monthly) - PostgreSQL and SQLite database support - Real-time countdown and payment animations - Swagger API documentation - Docker support Stack: - Backend: Node.js, TypeScript, Express - Frontend: Next.js, React, TailwindCSS, Redux - Payments: LNbits
This commit is contained in:
214
front_end/src/components/DrawAnimation.tsx
Normal file
214
front_end/src/components/DrawAnimation.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
interface DrawAnimationProps {
|
||||
winnerName: string;
|
||||
winningTicket: number;
|
||||
potAmount: number;
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
export function DrawAnimation({
|
||||
winnerName,
|
||||
winningTicket,
|
||||
potAmount,
|
||||
onComplete,
|
||||
}: DrawAnimationProps) {
|
||||
const [phase, setPhase] = useState<'spinning' | 'revealing' | 'winner' | 'done'>('spinning');
|
||||
const [displayTicket, setDisplayTicket] = useState(0);
|
||||
const [showConfetti, setShowConfetti] = useState(false);
|
||||
|
||||
// Generate random ticket numbers during spin
|
||||
useEffect(() => {
|
||||
if (phase !== 'spinning') return;
|
||||
|
||||
const spinInterval = setInterval(() => {
|
||||
setDisplayTicket(Math.floor(Math.random() * 999999999) + 1);
|
||||
}, 50);
|
||||
|
||||
// After 2.5 seconds, start revealing
|
||||
const revealTimeout = setTimeout(() => {
|
||||
setPhase('revealing');
|
||||
}, 2500);
|
||||
|
||||
return () => {
|
||||
clearInterval(spinInterval);
|
||||
clearTimeout(revealTimeout);
|
||||
};
|
||||
}, [phase]);
|
||||
|
||||
// Slow down and reveal actual number
|
||||
useEffect(() => {
|
||||
if (phase !== 'revealing') return;
|
||||
|
||||
let speed = 50;
|
||||
let iterations = 0;
|
||||
const maxIterations = 15;
|
||||
|
||||
const slowDown = () => {
|
||||
if (iterations >= maxIterations) {
|
||||
setDisplayTicket(winningTicket);
|
||||
setPhase('winner');
|
||||
return;
|
||||
}
|
||||
|
||||
speed += 30;
|
||||
iterations++;
|
||||
setDisplayTicket(Math.floor(Math.random() * 999999999) + 1);
|
||||
|
||||
setTimeout(slowDown, speed);
|
||||
};
|
||||
|
||||
slowDown();
|
||||
}, [phase, winningTicket]);
|
||||
|
||||
// Show winner and confetti
|
||||
useEffect(() => {
|
||||
if (phase !== 'winner') return;
|
||||
|
||||
setShowConfetti(true);
|
||||
|
||||
// Auto-dismiss after 6 seconds
|
||||
const dismissTimeout = setTimeout(() => {
|
||||
setPhase('done');
|
||||
onComplete();
|
||||
}, 6000);
|
||||
|
||||
return () => clearTimeout(dismissTimeout);
|
||||
}, [phase, onComplete]);
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
setPhase('done');
|
||||
onComplete();
|
||||
}, [onComplete]);
|
||||
|
||||
if (phase === 'done') return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/90 backdrop-blur-sm"
|
||||
onClick={phase === 'winner' ? handleDismiss : undefined}
|
||||
>
|
||||
{/* Confetti Effect */}
|
||||
{showConfetti && (
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
{[...Array(50)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="confetti-piece"
|
||||
style={{
|
||||
left: `${Math.random() * 100}%`,
|
||||
animationDelay: `${Math.random() * 2}s`,
|
||||
backgroundColor: ['#f7931a', '#ffd700', '#ff6b6b', '#4ecdc4', '#45b7d1'][
|
||||
Math.floor(Math.random() * 5)
|
||||
],
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-center px-6 max-w-lg">
|
||||
{/* Spinning Phase */}
|
||||
{(phase === 'spinning' || phase === 'revealing') && (
|
||||
<>
|
||||
<div className="text-2xl text-yellow-400 mb-4 animate-pulse">
|
||||
🎰 Drawing Winner...
|
||||
</div>
|
||||
<div className="bg-gray-900 rounded-2xl p-8 border-2 border-yellow-500/50 shadow-2xl shadow-yellow-500/20">
|
||||
<div className="text-gray-400 text-sm mb-2">Ticket Number</div>
|
||||
<div
|
||||
className={`text-5xl md:text-6xl font-mono font-bold text-bitcoin-orange ${
|
||||
phase === 'spinning' ? 'animate-number-spin' : ''
|
||||
}`}
|
||||
>
|
||||
#{displayTicket.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Winner Phase */}
|
||||
{phase === 'winner' && (
|
||||
<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">
|
||||
We Have a Winner!
|
||||
</div>
|
||||
<div className="bg-gradient-to-br from-yellow-900/60 to-orange-900/60 rounded-2xl p-8 border-2 border-yellow-500 shadow-2xl shadow-yellow-500/30">
|
||||
<div className="text-gray-300 text-sm mb-1">Winner</div>
|
||||
<div className="text-3xl md:text-4xl font-bold text-white mb-4">
|
||||
{winnerName || 'Anon'}
|
||||
</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()}
|
||||
</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
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 text-gray-400 text-sm animate-pulse">
|
||||
Click anywhere to continue
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.confetti-piece {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
top: -10px;
|
||||
animation: confetti-fall 4s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes confetti-fall {
|
||||
0% {
|
||||
transform: translateY(0) rotate(0deg);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(100vh) rotate(720deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-number-spin {
|
||||
animation: number-glow 0.1s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes number-glow {
|
||||
from {
|
||||
text-shadow: 0 0 10px rgba(247, 147, 26, 0.5);
|
||||
}
|
||||
to {
|
||||
text-shadow: 0 0 20px rgba(247, 147, 26, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-winner-reveal {
|
||||
animation: winner-pop 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes winner-pop {
|
||||
0% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user