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:
Michilis
2025-11-27 22:13:37 +00:00
commit d3bf8080b6
75 changed files with 18184 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
'use client';
import { useEffect, useState } from 'react';
import api from '@/lib/api';
import { LoadingSpinner } from '@/components/LoadingSpinner';
import STRINGS from '@/constants/strings';
import { formatDateTime } from '@/lib/format';
interface PastWin {
cycle_id: string;
cycle_type: string;
scheduled_at: string;
pot_total_sats: number;
pot_after_fee_sats: number | null;
winner_name: string;
winning_ticket_serial: number | null;
}
export default function PastWinsPage() {
const [wins, setWins] = useState<PastWin[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const loadWins = async () => {
try {
const response = await api.getPastWins();
setWins(response.data?.wins || []);
} catch (err: any) {
setError(err.message || 'Failed to load past wins');
} finally {
setLoading(false);
}
};
loadWins();
}, []);
if (loading) {
return <LoadingSpinner />;
}
if (error) {
return (
<div className="max-w-3xl mx-auto text-center py-12">
<div className="text-red-500 text-xl mb-2"> {error}</div>
<p className="text-gray-400">Please try again in a moment.</p>
</div>
);
}
return (
<div className="max-w-5xl mx-auto">
<div className="text-center mb-10">
<h1 className="text-3xl md:text-4xl font-bold text-white mb-3">
{STRINGS.pastWins.title}
</h1>
<p className="text-gray-400">{STRINGS.pastWins.description}</p>
</div>
{wins.length === 0 ? (
<div className="bg-gray-900 rounded-xl p-12 text-center border border-gray-800">
<div className="text-4xl mb-4"></div>
<div className="text-gray-300 text-lg">{STRINGS.pastWins.noWins}</div>
</div>
) : (
<div className="space-y-4">
{wins.map((win) => (
<div
key={win.cycle_id}
className="bg-gray-900 rounded-xl p-6 border border-gray-800"
>
<div className="flex flex-col md:flex-row md:items-center md:justify-between mb-4">
<div>
<div className="text-sm uppercase tracking-wide text-gray-500">
{win.cycle_type} {formatDateTime(win.scheduled_at)}
</div>
<div className="text-white font-mono text-sm">
{win.cycle_id.substring(0, 16)}...
</div>
</div>
<div className="mt-4 md:mt-0 text-right">
<div className="text-gray-400 text-sm">{STRINGS.pastWins.pot}</div>
<div className="text-2xl font-bold text-bitcoin-orange">
{win.pot_total_sats.toLocaleString()} sats
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div>
<div className="text-gray-400 mb-1">{STRINGS.pastWins.winner}</div>
<div className="text-white font-semibold">
{win.winner_name || 'Anon'}
</div>
</div>
<div>
<div className="text-gray-400 mb-1">{STRINGS.pastWins.ticket}</div>
<div className="text-white">
{win.winning_ticket_serial !== null
? `#${win.winning_ticket_serial}`
: 'N/A'}
</div>
</div>
<div>
<div className="text-gray-400 mb-1">{STRINGS.pastWins.drawTime}</div>
<div className="text-white">{formatDateTime(win.scheduled_at)}</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
);
}