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:
192
front_end/src/app/tickets/[id]/page.tsx
Normal file
192
front_end/src/app/tickets/[id]/page.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import api from '@/lib/api';
|
||||
import { JackpotCountdown } from '@/components/JackpotCountdown';
|
||||
import { TicketList } from '@/components/TicketList';
|
||||
import { PayoutStatus } from '@/components/PayoutStatus';
|
||||
import { LoadingSpinner } from '@/components/LoadingSpinner';
|
||||
import { formatDateTime } from '@/lib/format';
|
||||
import STRINGS from '@/constants/strings';
|
||||
|
||||
export default function TicketStatusPage() {
|
||||
const params = useParams();
|
||||
const ticketId = params.id as string;
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [autoRefresh, setAutoRefresh] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadTicketStatus();
|
||||
|
||||
// Auto-refresh if payment pending or draw not complete
|
||||
const interval = setInterval(() => {
|
||||
if (autoRefresh) {
|
||||
loadTicketStatus(true);
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [ticketId, autoRefresh]);
|
||||
|
||||
const loadTicketStatus = async (silent = false) => {
|
||||
try {
|
||||
if (!silent) setLoading(true);
|
||||
|
||||
const response = await api.getTicketStatus(ticketId);
|
||||
|
||||
if (response.data) {
|
||||
setData(response.data);
|
||||
|
||||
// Stop auto-refresh if payment is complete and draw is done
|
||||
if (
|
||||
response.data.purchase.invoice_status === 'paid' &&
|
||||
response.data.cycle.status === 'completed'
|
||||
) {
|
||||
setAutoRefresh(false);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to load ticket status');
|
||||
} finally {
|
||||
if (!silent) setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-red-500 text-xl mb-4">⚠️ {error}</div>
|
||||
<button
|
||||
onClick={() => loadTicketStatus()}
|
||||
className="bg-bitcoin-orange hover:bg-orange-600 text-white px-6 py-2 rounded-lg"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <div className="text-center py-12 text-gray-400">Ticket not found</div>;
|
||||
}
|
||||
|
||||
const { purchase, tickets, cycle, result } = data;
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h1 className="text-3xl md:text-4xl font-bold mb-8 text-center text-white">
|
||||
{STRINGS.ticket.title}
|
||||
</h1>
|
||||
|
||||
{/* Purchase Info */}
|
||||
<div className="bg-gray-900 rounded-xl p-6 mb-6 border border-gray-800">
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-400">Purchase ID:</span>
|
||||
<div className="text-white font-mono break-all">{purchase.id}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-400">Status:</span>
|
||||
<div className="text-white capitalize">{purchase.invoice_status}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-400">Tickets:</span>
|
||||
<div className="text-white">{purchase.number_of_tickets}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-400">Amount:</span>
|
||||
<div className="text-white">{purchase.amount_sats.toLocaleString()} sats</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment Status */}
|
||||
{purchase.invoice_status === 'pending' && (
|
||||
<div className="bg-yellow-900/30 text-yellow-200 px-6 py-4 rounded-lg mb-6 text-center">
|
||||
{STRINGS.ticket.waiting}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tickets */}
|
||||
{purchase.ticket_issue_status === 'issued' && (
|
||||
<div className="bg-gray-900 rounded-xl p-6 mb-6 border border-gray-800">
|
||||
<h2 className="text-xl font-semibold mb-4 text-gray-300">
|
||||
{STRINGS.ticket.ticketNumbers}
|
||||
</h2>
|
||||
<TicketList tickets={tickets} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Draw Info */}
|
||||
<div className="bg-gray-900 rounded-xl p-6 mb-6 border border-gray-800">
|
||||
<h2 className="text-xl font-semibold mb-4 text-gray-300">
|
||||
Draw Information
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<span className="text-gray-400">Draw Time:</span>
|
||||
<div className="text-white">{formatDateTime(cycle.scheduled_at)}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="text-gray-400">Current Pot:</span>
|
||||
<div className="text-2xl font-bold text-bitcoin-orange">
|
||||
{cycle.pot_total_sats.toLocaleString()} sats
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{cycle.status !== 'completed' && (
|
||||
<div>
|
||||
<span className="text-gray-400 block mb-2">Time Until Draw:</span>
|
||||
<JackpotCountdown scheduledAt={cycle.scheduled_at} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Results */}
|
||||
{result.has_drawn && (
|
||||
<div className="bg-gray-900 rounded-xl p-6 border border-gray-800">
|
||||
<h2 className="text-xl font-semibold mb-4 text-gray-300">
|
||||
Draw Results
|
||||
</h2>
|
||||
|
||||
{result.is_winner ? (
|
||||
<div>
|
||||
<div className="bg-green-900/30 text-green-200 px-6 py-4 rounded-lg mb-4 text-center text-2xl font-bold">
|
||||
🎉 {STRINGS.ticket.congratulations}
|
||||
</div>
|
||||
{result.payout && (
|
||||
<PayoutStatus
|
||||
status={result.payout.status}
|
||||
amountSats={result.payout.amount_sats}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="bg-gray-800 px-6 py-4 rounded-lg mb-4 text-center">
|
||||
<div className="text-gray-400 mb-2">{STRINGS.ticket.betterLuck}</div>
|
||||
{cycle.winning_ticket_id && (
|
||||
<div className="text-gray-300">
|
||||
{STRINGS.ticket.winningTicket}: <span className="font-bold text-bitcoin-orange">#{cycle.winning_ticket_id.substring(0, 8)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user