Files
LightningLotto/front_end/src/app/tickets/[id]/page.tsx
Michilis dd6b26c524 feat: Mobile optimization and UI improvements
- Add mobile hamburger menu in TopBar with slide-in panel
- Optimize all components for mobile (responsive fonts, spacing, touch targets)
- Add proper viewport meta tags and safe area padding
- Fix /jackpot/next API to return active cycles regardless of scheduled time
- Remove BTC display from jackpot pot (show sats only)
- Add setup/ folder to .gitignore
- Improve mobile UX: 16px inputs (no iOS zoom), 44px touch targets
- Add active states for touch feedback on buttons
2025-12-08 15:51:13 +00:00

252 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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);
const [copied, setCopied] = useState(false);
const ticketUrl = typeof window !== 'undefined'
? `${window.location.origin}/tickets/${ticketId}`
: '';
const copyLink = async () => {
try {
await navigator.clipboard.writeText(ticketUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
};
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 px-1">
<h1 className="text-2xl sm:text-3xl md:text-4xl font-bold mb-6 sm:mb-8 text-center text-white">
{STRINGS.ticket.title}
</h1>
{/* Save This Link */}
<div className="bg-gradient-to-r from-blue-900/30 to-purple-900/30 border border-blue-700/50 rounded-xl p-4 sm:p-6 mb-5 sm:mb-6">
<div className="flex items-start gap-3 sm:gap-4">
<div className="text-2xl sm:text-3xl">🔖</div>
<div className="flex-1 min-w-0">
<h3 className="text-base sm:text-lg font-semibold text-white mb-1 sm:mb-2">Save This Link!</h3>
<p className="text-gray-300 text-xs sm:text-sm mb-3">
Bookmark or save this page to check if you've won after the draw. This is your only way to view your ticket status.
</p>
<div className="flex flex-col gap-2">
<div className="bg-gray-800/80 rounded-lg px-3 py-2 font-mono text-xs sm:text-sm text-gray-300 break-all overflow-x-auto">
{ticketUrl || `/tickets/${ticketId}`}
</div>
<button
onClick={copyLink}
className={`w-full sm:w-auto px-4 py-2.5 rounded-lg font-medium transition-all flex items-center justify-center gap-2 ${
copied
? 'bg-green-600 text-white'
: 'bg-blue-600 hover:bg-blue-500 active:bg-blue-700 text-white'
}`}
>
{copied ? (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
Copied!
</>
) : (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
</svg>
Copy Link
</>
)}
</button>
</div>
</div>
</div>
</div>
{/* Purchase Info */}
<div className="bg-gray-900 rounded-xl p-4 sm:p-6 mb-5 sm:mb-6 border border-gray-800">
<div className="grid grid-cols-2 gap-3 sm:gap-4 text-xs sm:text-sm">
<div>
<span className="text-gray-400">Purchase ID:</span>
<div className="text-white font-mono break-all text-xs sm:text-sm">{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-4 sm:px-6 py-3 sm:py-4 rounded-lg mb-5 sm:mb-6 text-center text-sm sm:text-base">
{STRINGS.ticket.waiting}
</div>
)}
{/* Tickets */}
{purchase.ticket_issue_status === 'issued' && (
<div className="bg-gray-900 rounded-xl p-4 sm:p-6 mb-5 sm:mb-6 border border-gray-800">
<h2 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4 text-gray-300">
{STRINGS.ticket.ticketNumbers}
</h2>
<TicketList tickets={tickets} />
</div>
)}
{/* Draw Info */}
<div className="bg-gray-900 rounded-xl p-4 sm:p-6 mb-5 sm:mb-6 border border-gray-800">
<h2 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4 text-gray-300">
Draw Information
</h2>
<div className="space-y-3 sm:space-y-4">
<div>
<span className="text-gray-400 text-sm">Draw Time:</span>
<div className="text-white text-sm sm:text-base">{formatDateTime(cycle.scheduled_at)}</div>
</div>
<div>
<span className="text-gray-400 text-sm">Current Pot:</span>
<div className="text-xl sm:text-2xl font-bold text-bitcoin-orange">
{cycle.pot_total_sats.toLocaleString()} sats
</div>
</div>
{cycle.status !== 'completed' && (
<div>
<span className="text-gray-400 text-sm block mb-2">Time Until Draw:</span>
<div className="overflow-x-auto scrollbar-hide">
<JackpotCountdown scheduledAt={cycle.scheduled_at} />
</div>
</div>
)}
</div>
</div>
{/* Results */}
{result.has_drawn && (
<div className="bg-gray-900 rounded-xl p-4 sm:p-6 border border-gray-800">
<h2 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4 text-gray-300">
Draw Results
</h2>
{result.is_winner ? (
<div>
<div className="bg-green-900/30 text-green-200 px-4 sm:px-6 py-3 sm:py-4 rounded-lg mb-4 text-center text-xl sm: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-4 sm:px-6 py-3 sm:py-4 rounded-lg mb-4 text-center">
<div className="text-gray-400 mb-2 text-sm sm:text-base">{STRINGS.ticket.betterLuck}</div>
{cycle.winning_ticket_id && (
<div className="text-gray-300 text-sm sm:text-base">
{STRINGS.ticket.winningTicket}: <span className="font-bold text-bitcoin-orange">#{cycle.winning_ticket_id.substring(0, 8)}</span>
</div>
)}
</div>
</div>
)}
</div>
)}
</div>
);
}