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
This commit is contained in:
Michilis
2025-12-08 15:51:13 +00:00
parent 2fea2dc836
commit dd6b26c524
13 changed files with 432 additions and 170 deletions

2
.gitignore vendored
View File

@@ -88,3 +88,5 @@ ehthumbs.db
*.key *.key
secrets/ secrets/
# Setup/deployment configs (contains server-specific settings)
setup/

View File

@@ -65,17 +65,29 @@ export async function getNextJackpot(req: Request, res: Response) {
const lottery = lotteryResult.rows[0]; const lottery = lotteryResult.rows[0];
// Get next cycle // Get next cycle - first try to find one that hasn't drawn yet
const cycleResult = await db.query<JackpotCycle>( let cycleResult = await db.query<JackpotCycle>(
`SELECT * FROM jackpot_cycles `SELECT * FROM jackpot_cycles
WHERE lottery_id = $1 WHERE lottery_id = $1
AND status IN ('scheduled', 'sales_open') AND status IN ('scheduled', 'sales_open', 'drawing')
AND scheduled_at > NOW()
ORDER BY scheduled_at ASC ORDER BY scheduled_at ASC
LIMIT 1`, LIMIT 1`,
[lottery.id] [lottery.id]
); );
// If no active cycles, get the next upcoming one
if (cycleResult.rows.length === 0) {
cycleResult = await db.query<JackpotCycle>(
`SELECT * FROM jackpot_cycles
WHERE lottery_id = $1
AND status = 'scheduled'
AND scheduled_at > NOW()
ORDER BY scheduled_at ASC
LIMIT 1`,
[lottery.id]
);
}
if (cycleResult.rows.length === 0) { if (cycleResult.rows.length === 0) {
return res.status(503).json({ return res.status(503).json({
version: '1.0', version: '1.0',

View File

@@ -222,15 +222,15 @@ export default function BuyPage() {
const totalCost = ticketPriceSats * tickets; const totalCost = ticketPriceSats * tickets;
return ( return (
<div className="max-w-2xl mx-auto"> <div className="max-w-2xl mx-auto px-1">
<h1 className="text-3xl md:text-4xl font-bold mb-8 text-center text-white"> <h1 className="text-2xl sm:text-3xl md:text-4xl font-bold mb-6 sm:mb-8 text-center text-white">
{STRINGS.buy.title} {STRINGS.buy.title}
</h1> </h1>
{!invoice ? ( {!invoice ? (
/* Purchase Form */ /* Purchase Form */
<div className="bg-gray-900 rounded-xl p-8 border border-gray-800"> <div className="bg-gray-900 rounded-xl p-5 sm:p-8 border border-gray-800">
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-5 sm:space-y-6">
{/* Lightning Address */} {/* Lightning Address */}
<div> <div>
<label className="block text-gray-300 mb-2 font-medium"> <label className="block text-gray-300 mb-2 font-medium">
@@ -321,7 +321,7 @@ export default function BuyPage() {
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="w-full bg-bitcoin-orange hover:bg-orange-600 disabled:bg-gray-600 text-white py-4 rounded-lg text-lg font-bold transition-colors" className="w-full bg-bitcoin-orange hover:bg-orange-600 active:bg-orange-700 disabled:bg-gray-600 text-white py-3.5 sm:py-4 rounded-lg text-base sm:text-lg font-bold transition-colors"
> >
{loading ? 'Creating Invoice...' : STRINGS.buy.createInvoice} {loading ? 'Creating Invoice...' : STRINGS.buy.createInvoice}
</button> </button>

View File

@@ -10,8 +10,17 @@
body { body {
color: rgb(var(--foreground-rgb)); color: rgb(var(--foreground-rgb));
background: rgb(var(--background-rgb)); background: rgb(var(--background-rgb));
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
} }
/* Prevent horizontal overflow on mobile */
html, body {
overflow-x: hidden;
max-width: 100vw;
}
/* Touch-friendly tap targets */
@layer utilities { @layer utilities {
.text-balance { .text-balance {
text-wrap: balance; text-wrap: balance;
@@ -20,6 +29,39 @@ body {
.animate-fade-in { .animate-fade-in {
animation: fade-in 0.5s ease-out forwards; animation: fade-in 0.5s ease-out forwards;
} }
/* Mobile-friendly tap targets */
.touch-target {
min-height: 44px;
min-width: 44px;
}
/* Hide scrollbar while maintaining functionality */
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
/* Safe area padding for notched devices */
.safe-top {
padding-top: env(safe-area-inset-top);
}
.safe-bottom {
padding-bottom: env(safe-area-inset-bottom);
}
.safe-left {
padding-left: env(safe-area-inset-left);
}
.safe-right {
padding-right: env(safe-area-inset-right);
}
} }
@keyframes fade-in { @keyframes fade-in {
@@ -33,21 +75,68 @@ body {
} }
} }
/* Custom scrollbar */ /* Custom scrollbar (desktop) */
::-webkit-scrollbar { @media (hover: hover) and (pointer: fine) {
width: 8px; ::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #0b0b0b;
}
::-webkit-scrollbar-thumb {
background: #333;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
} }
::-webkit-scrollbar-track { /* Mobile-specific styles */
background: #0b0b0b; @media (max-width: 640px) {
/* Larger touch targets on mobile */
button,
a,
input[type="button"],
input[type="submit"],
[role="button"] {
min-height: 44px;
}
/* Better input styling on mobile */
input, textarea, select {
font-size: 16px; /* Prevents zoom on iOS */
}
/* Smoother scrolling on mobile */
* {
-webkit-overflow-scrolling: touch;
}
} }
::-webkit-scrollbar-thumb { /* Prevent iOS bounce/pull-to-refresh on certain elements */
background: #333; .no-bounce {
border-radius: 4px; overscroll-behavior: none;
} }
::-webkit-scrollbar-thumb:hover { /* Focus visible only for keyboard navigation */
background: #555; @media (hover: none) and (pointer: coarse) {
*:focus {
outline: none;
}
} }
/* Print styles */
@media print {
nav, footer, button {
display: none !important;
}
body {
background: white;
color: black;
}
}

View File

@@ -1,4 +1,4 @@
import type { Metadata } from 'next'; import type { Metadata, Viewport } from 'next';
import { Inter } from 'next/font/google'; import { Inter } from 'next/font/google';
import './globals.css'; import './globals.css';
import { Providers } from './providers'; import { Providers } from './providers';
@@ -7,9 +7,25 @@ import { Footer } from '@/components/Footer';
const inter = Inter({ subsets: ['latin'] }); const inter = Inter({ subsets: ['latin'] });
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
maximumScale: 5,
userScalable: true,
themeColor: '#0b0b0b',
};
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Lightning Lottery - Win Bitcoin', title: 'Lightning Lottery - Win Bitcoin',
description: 'Bitcoin Lightning Network powered lottery with instant payouts', description: 'Bitcoin Lightning Network powered lottery with instant payouts',
appleWebApp: {
capable: true,
statusBarStyle: 'black-translucent',
title: 'Lightning Lotto',
},
formatDetection: {
telephone: false,
},
}; };
export default function RootLayout({ export default function RootLayout({

View File

@@ -246,20 +246,20 @@ export default function HomePage() {
)} )}
{/* Hero Section */} {/* Hero Section */}
<div className="text-center mb-12"> <div className="text-center mb-8 sm:mb-12">
<h1 className="text-4xl md:text-6xl font-bold mb-4 text-white"> <h1 className="text-3xl sm:text-4xl md:text-6xl font-bold mb-3 sm:mb-4 text-white">
{STRINGS.app.title} {STRINGS.app.title}
</h1> </h1>
<p className="text-xl text-gray-400">{STRINGS.app.tagline}</p> <p className="text-lg sm:text-xl text-gray-400">{STRINGS.app.tagline}</p>
</div> </div>
{/* Recent Winner Banner - Only shown for 60 seconds after draw */} {/* Recent Winner Banner - Only shown for 60 seconds after draw */}
{showWinnerBanner && ( {showWinnerBanner && (
<div className="bg-gradient-to-r from-yellow-900/40 via-yellow-800/30 to-yellow-900/40 border border-yellow-600/50 rounded-2xl p-6 mb-8 animate-fade-in relative"> <div className="bg-gradient-to-r from-yellow-900/40 via-yellow-800/30 to-yellow-900/40 border border-yellow-600/50 rounded-xl sm:rounded-2xl p-4 sm:p-6 mb-6 sm:mb-8 animate-fade-in relative">
{/* Close button */} {/* Close button */}
<button <button
onClick={handleDismissWinnerBanner} onClick={handleDismissWinnerBanner}
className="absolute top-3 right-3 text-yellow-400/60 hover:text-yellow-400 transition-colors p-1" className="absolute top-2 right-2 sm:top-3 sm:right-3 text-yellow-400/60 hover:text-yellow-400 transition-colors p-2"
aria-label="Dismiss" aria-label="Dismiss"
> >
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -267,16 +267,16 @@ export default function HomePage() {
</svg> </svg>
</button> </button>
<div className="text-center"> <div className="text-center">
<div className="text-yellow-400 text-sm uppercase tracking-wider mb-2"> <div className="text-yellow-400 text-xs sm:text-sm uppercase tracking-wider mb-2">
🏆 Latest Winner 🏆 Latest Winner
</div> </div>
<div className="text-2xl md:text-3xl font-bold text-white mb-2"> <div className="text-xl sm:text-2xl md:text-3xl font-bold text-white mb-2">
{recentWinner.winner_name || 'Anon'} {recentWinner.winner_name || 'Anon'}
</div> </div>
<div className="text-yellow-400 text-xl font-mono mb-3"> <div className="text-yellow-400 text-lg sm:text-xl font-mono mb-2 sm:mb-3">
Won {recentWinner.pot_after_fee_sats.toLocaleString()} sats Won {recentWinner.pot_after_fee_sats.toLocaleString()} sats
</div> </div>
<div className="text-gray-400 text-sm"> <div className="text-gray-400 text-xs sm:text-sm">
Ticket #{recentWinner.winning_ticket_serial.toLocaleString()} Ticket #{recentWinner.winning_ticket_serial.toLocaleString()}
</div> </div>
</div> </div>
@@ -284,22 +284,22 @@ export default function HomePage() {
)} )}
{/* Current Jackpot Card */} {/* Current Jackpot Card */}
<div className="bg-gray-900 rounded-2xl p-8 md:p-12 mb-8 border border-gray-800"> <div className="bg-gray-900 rounded-xl sm:rounded-2xl p-5 sm:p-8 md:p-12 mb-6 sm:mb-8 border border-gray-800">
<h2 className="text-2xl font-semibold text-center mb-6 text-gray-300"> <h2 className="text-xl sm:text-2xl font-semibold text-center mb-4 sm:mb-6 text-gray-300">
{STRINGS.home.currentJackpot} {STRINGS.home.currentJackpot}
</h2> </h2>
{/* Pot Display */} {/* Pot Display */}
<div className="mb-8"> <div className="mb-6 sm:mb-8">
<JackpotPotDisplay potTotalSats={jackpot.cycle.pot_total_sats} /> <JackpotPotDisplay potTotalSats={jackpot.cycle.pot_total_sats} />
</div> </div>
{/* Countdown */} {/* Countdown */}
<div className="mb-8"> <div className="mb-6 sm:mb-8">
<div className="text-center text-gray-400 mb-4"> <div className="text-center text-gray-400 mb-3 sm:mb-4 text-sm sm:text-base">
{STRINGS.home.drawIn} {STRINGS.home.drawIn}
</div> </div>
<div className="flex justify-center"> <div className="flex justify-center overflow-x-auto scrollbar-hide">
<JackpotCountdown <JackpotCountdown
scheduledAt={jackpot.cycle.scheduled_at} scheduledAt={jackpot.cycle.scheduled_at}
drawCompleted={awaitingNextCycle || drawJustCompleted} drawCompleted={awaitingNextCycle || drawJustCompleted}
@@ -308,7 +308,7 @@ export default function HomePage() {
</div> </div>
{/* Ticket Price */} {/* Ticket Price */}
<div className="text-center text-gray-400 mb-8"> <div className="text-center text-gray-400 mb-6 sm:mb-8 text-sm sm:text-base">
Ticket Price: {jackpot.lottery.ticket_price_sats.toLocaleString()} sats Ticket Price: {jackpot.lottery.ticket_price_sats.toLocaleString()} sats
</div> </div>
@@ -317,7 +317,7 @@ export default function HomePage() {
<div className="flex justify-center"> <div className="flex justify-center">
<Link <Link
href="/buy" href="/buy"
className="bg-bitcoin-orange hover:bg-orange-600 text-white px-12 py-4 rounded-lg text-xl font-bold transition-colors shadow-lg text-center" className="bg-bitcoin-orange hover:bg-orange-600 active:bg-orange-700 text-white px-8 sm:px-12 py-3 sm:py-4 rounded-lg text-lg sm:text-xl font-bold transition-colors shadow-lg text-center w-full sm:w-auto"
> >
{STRINGS.home.buyTickets} {STRINGS.home.buyTickets}
</Link> </Link>
@@ -326,8 +326,8 @@ export default function HomePage() {
</div> </div>
{/* Check Ticket Section */} {/* Check Ticket Section */}
<div className="bg-gray-900 rounded-2xl p-8 border border-gray-800"> <div className="bg-gray-900 rounded-xl sm:rounded-2xl p-5 sm:p-8 border border-gray-800">
<h3 className="text-xl font-semibold text-center mb-4 text-gray-300"> <h3 className="text-lg sm:text-xl font-semibold text-center mb-4 text-gray-300">
{STRINGS.home.checkTicket} {STRINGS.home.checkTicket}
</h3> </h3>
<div className="flex flex-col sm:flex-row gap-3"> <div className="flex flex-col sm:flex-row gap-3">
@@ -336,12 +336,12 @@ export default function HomePage() {
value={ticketId} value={ticketId}
onChange={(e) => setTicketId(e.target.value)} onChange={(e) => setTicketId(e.target.value)}
placeholder={STRINGS.home.ticketIdPlaceholder} placeholder={STRINGS.home.ticketIdPlaceholder}
className="flex-1 bg-gray-800 text-white px-4 py-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-bitcoin-orange" className="flex-1 bg-gray-800 text-white px-4 py-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-bitcoin-orange text-base"
onKeyPress={(e) => e.key === 'Enter' && handleCheckTicket()} onKeyPress={(e) => e.key === 'Enter' && handleCheckTicket()}
/> />
<button <button
onClick={handleCheckTicket} onClick={handleCheckTicket}
className="bg-gray-700 hover:bg-gray-600 text-white px-8 py-3 rounded-lg font-medium transition-colors" className="bg-gray-700 hover:bg-gray-600 active:bg-gray-500 text-white px-6 sm:px-8 py-3 rounded-lg font-medium transition-colors"
> >
Check Status Check Status
</button> </button>
@@ -349,25 +349,25 @@ export default function HomePage() {
</div> </div>
{/* Info Section */} {/* Info Section */}
<div className="mt-12 grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="mt-8 sm:mt-12 grid grid-cols-1 sm:grid-cols-3 gap-4 sm:gap-6">
<div className="bg-gray-900 p-6 rounded-lg border border-gray-800"> <div className="bg-gray-900 p-5 sm:p-6 rounded-lg border border-gray-800">
<div className="text-4xl mb-3"></div> <div className="text-3xl sm:text-4xl mb-2 sm:mb-3"></div>
<h4 className="text-lg font-semibold mb-2 text-white">Instant</h4> <h4 className="text-base sm:text-lg font-semibold mb-1 sm:mb-2 text-white">Instant</h4>
<p className="text-gray-400 text-sm"> <p className="text-gray-400 text-xs sm:text-sm">
Lightning-fast ticket purchases and payouts Lightning-fast ticket purchases and payouts
</p> </p>
</div> </div>
<div className="bg-gray-900 p-6 rounded-lg border border-gray-800"> <div className="bg-gray-900 p-5 sm:p-6 rounded-lg border border-gray-800">
<div className="text-4xl mb-3">🔒</div> <div className="text-3xl sm:text-4xl mb-2 sm:mb-3">🔒</div>
<h4 className="text-lg font-semibold mb-2 text-white">Secure</h4> <h4 className="text-base sm:text-lg font-semibold mb-1 sm:mb-2 text-white">Secure</h4>
<p className="text-gray-400 text-sm"> <p className="text-gray-400 text-xs sm:text-sm">
Cryptographically secure random number generation Cryptographically secure random number generation
</p> </p>
</div> </div>
<div className="bg-gray-900 p-6 rounded-lg border border-gray-800"> <div className="bg-gray-900 p-5 sm:p-6 rounded-lg border border-gray-800">
<div className="text-4xl mb-3">🎯</div> <div className="text-3xl sm:text-4xl mb-2 sm:mb-3">🎯</div>
<h4 className="text-lg font-semibold mb-2 text-white">Fair</h4> <h4 className="text-base sm:text-lg font-semibold mb-1 sm:mb-2 text-white">Fair</h4>
<p className="text-gray-400 text-sm"> <p className="text-gray-400 text-xs sm:text-sm">
Transparent draws with verifiable results Transparent draws with verifiable results
</p> </p>
</div> </div>

View File

@@ -96,30 +96,30 @@ export default function TicketStatusPage() {
const { purchase, tickets, cycle, result } = data; const { purchase, tickets, cycle, result } = data;
return ( return (
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto px-1">
<h1 className="text-3xl md:text-4xl font-bold mb-8 text-center text-white"> <h1 className="text-2xl sm:text-3xl md:text-4xl font-bold mb-6 sm:mb-8 text-center text-white">
{STRINGS.ticket.title} {STRINGS.ticket.title}
</h1> </h1>
{/* Save This Link */} {/* 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-6 mb-6"> <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-4"> <div className="flex items-start gap-3 sm:gap-4">
<div className="text-3xl">🔖</div> <div className="text-2xl sm:text-3xl">🔖</div>
<div className="flex-1"> <div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold text-white mb-2">Save This Link!</h3> <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-sm mb-3"> <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. 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> </p>
<div className="flex flex-col sm:flex-row gap-2"> <div className="flex flex-col gap-2">
<div className="flex-1 bg-gray-800/80 rounded-lg px-3 py-2 font-mono text-sm text-gray-300 break-all"> <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}`} {ticketUrl || `/tickets/${ticketId}`}
</div> </div>
<button <button
onClick={copyLink} onClick={copyLink}
className={`px-4 py-2 rounded-lg font-medium transition-all flex items-center justify-center gap-2 ${ 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 copied
? 'bg-green-600 text-white' ? 'bg-green-600 text-white'
: 'bg-blue-600 hover:bg-blue-500 text-white' : 'bg-blue-600 hover:bg-blue-500 active:bg-blue-700 text-white'
}`} }`}
> >
{copied ? ( {copied ? (
@@ -144,11 +144,11 @@ export default function TicketStatusPage() {
</div> </div>
{/* Purchase Info */} {/* Purchase Info */}
<div className="bg-gray-900 rounded-xl p-6 mb-6 border border-gray-800"> <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-4 text-sm"> <div className="grid grid-cols-2 gap-3 sm:gap-4 text-xs sm:text-sm">
<div> <div>
<span className="text-gray-400">Purchase ID:</span> <span className="text-gray-400">Purchase ID:</span>
<div className="text-white font-mono break-all">{purchase.id}</div> <div className="text-white font-mono break-all text-xs sm:text-sm">{purchase.id}</div>
</div> </div>
<div> <div>
<span className="text-gray-400">Status:</span> <span className="text-gray-400">Status:</span>
@@ -167,15 +167,15 @@ export default function TicketStatusPage() {
{/* Payment Status */} {/* Payment Status */}
{purchase.invoice_status === 'pending' && ( {purchase.invoice_status === 'pending' && (
<div className="bg-yellow-900/30 text-yellow-200 px-6 py-4 rounded-lg mb-6 text-center"> <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} {STRINGS.ticket.waiting}
</div> </div>
)} )}
{/* Tickets */} {/* Tickets */}
{purchase.ticket_issue_status === 'issued' && ( {purchase.ticket_issue_status === 'issued' && (
<div className="bg-gray-900 rounded-xl p-6 mb-6 border border-gray-800"> <div className="bg-gray-900 rounded-xl p-4 sm:p-6 mb-5 sm:mb-6 border border-gray-800">
<h2 className="text-xl font-semibold mb-4 text-gray-300"> <h2 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4 text-gray-300">
{STRINGS.ticket.ticketNumbers} {STRINGS.ticket.ticketNumbers}
</h2> </h2>
<TicketList tickets={tickets} /> <TicketList tickets={tickets} />
@@ -183,28 +183,30 @@ export default function TicketStatusPage() {
)} )}
{/* Draw Info */} {/* Draw Info */}
<div className="bg-gray-900 rounded-xl p-6 mb-6 border border-gray-800"> <div className="bg-gray-900 rounded-xl p-4 sm:p-6 mb-5 sm:mb-6 border border-gray-800">
<h2 className="text-xl font-semibold mb-4 text-gray-300"> <h2 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4 text-gray-300">
Draw Information Draw Information
</h2> </h2>
<div className="space-y-4"> <div className="space-y-3 sm:space-y-4">
<div> <div>
<span className="text-gray-400">Draw Time:</span> <span className="text-gray-400 text-sm">Draw Time:</span>
<div className="text-white">{formatDateTime(cycle.scheduled_at)}</div> <div className="text-white text-sm sm:text-base">{formatDateTime(cycle.scheduled_at)}</div>
</div> </div>
<div> <div>
<span className="text-gray-400">Current Pot:</span> <span className="text-gray-400 text-sm">Current Pot:</span>
<div className="text-2xl font-bold text-bitcoin-orange"> <div className="text-xl sm:text-2xl font-bold text-bitcoin-orange">
{cycle.pot_total_sats.toLocaleString()} sats {cycle.pot_total_sats.toLocaleString()} sats
</div> </div>
</div> </div>
{cycle.status !== 'completed' && ( {cycle.status !== 'completed' && (
<div> <div>
<span className="text-gray-400 block mb-2">Time Until Draw:</span> <span className="text-gray-400 text-sm block mb-2">Time Until Draw:</span>
<JackpotCountdown scheduledAt={cycle.scheduled_at} /> <div className="overflow-x-auto scrollbar-hide">
<JackpotCountdown scheduledAt={cycle.scheduled_at} />
</div>
</div> </div>
)} )}
</div> </div>
@@ -212,14 +214,14 @@ export default function TicketStatusPage() {
{/* Results */} {/* Results */}
{result.has_drawn && ( {result.has_drawn && (
<div className="bg-gray-900 rounded-xl p-6 border border-gray-800"> <div className="bg-gray-900 rounded-xl p-4 sm:p-6 border border-gray-800">
<h2 className="text-xl font-semibold mb-4 text-gray-300"> <h2 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4 text-gray-300">
Draw Results Draw Results
</h2> </h2>
{result.is_winner ? ( {result.is_winner ? (
<div> <div>
<div className="bg-green-900/30 text-green-200 px-6 py-4 rounded-lg mb-4 text-center text-2xl font-bold"> <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} 🎉 {STRINGS.ticket.congratulations}
</div> </div>
{result.payout && ( {result.payout && (
@@ -231,10 +233,10 @@ export default function TicketStatusPage() {
</div> </div>
) : ( ) : (
<div> <div>
<div className="bg-gray-800 px-6 py-4 rounded-lg mb-4 text-center"> <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">{STRINGS.ticket.betterLuck}</div> <div className="text-gray-400 mb-2 text-sm sm:text-base">{STRINGS.ticket.betterLuck}</div>
{cycle.winning_ticket_id && ( {cycle.winning_ticket_id && (
<div className="text-gray-300"> <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> {STRINGS.ticket.winningTicket}: <span className="font-bold text-bitcoin-orange">#{cycle.winning_ticket_id.substring(0, 8)}</span>
</div> </div>
)} )}

View File

@@ -147,17 +147,17 @@ export function DrawAnimation({
</div> </div>
)} )}
<div className="text-center px-6 max-w-lg"> <div className="text-center px-4 sm:px-6 max-w-lg w-full">
{/* Spinning Phase */} {/* Spinning Phase */}
{(phase === 'spinning' || phase === 'revealing') && ( {(phase === 'spinning' || phase === 'revealing') && (
<> <>
<div className="text-2xl text-yellow-400 mb-4 animate-pulse"> <div className="text-xl sm:text-2xl text-yellow-400 mb-3 sm:mb-4 animate-pulse">
🎰 Drawing Winner... 🎰 Drawing Winner...
</div> </div>
<div className="bg-gray-900 rounded-2xl p-8 border-2 border-yellow-500/50 shadow-2xl shadow-yellow-500/20"> <div className="bg-gray-900 rounded-xl sm:rounded-2xl p-5 sm: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-gray-400 text-xs sm:text-sm mb-2">Ticket Number</div>
<div <div
className={`text-5xl md:text-6xl font-mono font-bold text-bitcoin-orange ${ className={`text-3xl sm:text-5xl md:text-6xl font-mono font-bold text-bitcoin-orange break-all ${
phase === 'spinning' ? 'animate-number-spin' : '' phase === 'spinning' ? 'animate-number-spin' : ''
}`} }`}
> >
@@ -170,26 +170,26 @@ export function DrawAnimation({
{/* Winner Phase */} {/* Winner Phase */}
{phase === 'winner' && hasWinner && ( {phase === 'winner' && hasWinner && (
<div className="animate-winner-reveal"> <div className="animate-winner-reveal">
<div className="text-4xl mb-4">🎉🏆🎉</div> <div className="text-3xl sm:text-4xl mb-3 sm:mb-4">🎉🏆🎉</div>
<div className="text-3xl md:text-4xl font-bold text-yellow-400 mb-6"> <div className="text-2xl sm:text-3xl md:text-4xl font-bold text-yellow-400 mb-4 sm:mb-6">
We Have a Winner! We Have a Winner!
</div> </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="bg-gradient-to-br from-yellow-900/60 to-orange-900/60 rounded-xl sm:rounded-2xl p-5 sm: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-gray-300 text-xs sm:text-sm mb-1">Winner</div>
<div className="text-3xl md:text-4xl font-bold text-white mb-4"> <div className="text-2xl sm:text-3xl md:text-4xl font-bold text-white mb-3 sm:mb-4 break-all">
{winnerName || 'Anon'} {winnerName || 'Anon'}
</div> </div>
<div className="text-gray-300 text-sm mb-1">Winning Ticket</div> <div className="text-gray-300 text-xs sm:text-sm mb-1">Winning Ticket</div>
<div className="text-2xl font-mono text-bitcoin-orange mb-4"> <div className="text-xl sm:text-2xl font-mono text-bitcoin-orange mb-3 sm:mb-4">
#{winningTicket!.toLocaleString()} #{winningTicket!.toLocaleString()}
</div> </div>
<div className="text-gray-300 text-sm mb-1">Prize</div> <div className="text-gray-300 text-xs sm:text-sm mb-1">Prize</div>
<div className="text-4xl md:text-5xl font-bold text-green-400"> <div className="text-3xl sm:text-4xl md:text-5xl font-bold text-green-400">
{potAmount!.toLocaleString()} sats {potAmount!.toLocaleString()} sats
</div> </div>
</div> </div>
<div className="mt-6 text-gray-400 text-sm animate-pulse"> <div className="mt-4 sm:mt-6 text-gray-400 text-xs sm:text-sm animate-pulse">
Click anywhere to continue Tap anywhere to continue
</div> </div>
</div> </div>
)} )}
@@ -197,20 +197,20 @@ export function DrawAnimation({
{/* No Winner Phase (no tickets sold) */} {/* No Winner Phase (no tickets sold) */}
{phase === 'no-winner' && ( {phase === 'no-winner' && (
<div className="animate-winner-reveal"> <div className="animate-winner-reveal">
<div className="text-4xl mb-4">😔</div> <div className="text-3xl sm:text-4xl mb-3 sm:mb-4">😔</div>
<div className="text-2xl md:text-3xl font-bold text-gray-400 mb-6"> <div className="text-xl sm:text-2xl md:text-3xl font-bold text-gray-400 mb-4 sm:mb-6">
No Tickets This Round No Tickets This Round
</div> </div>
<div className="bg-gray-900 rounded-2xl p-8 border-2 border-gray-600 shadow-2xl"> <div className="bg-gray-900 rounded-xl sm:rounded-2xl p-5 sm:p-8 border-2 border-gray-600 shadow-2xl">
<div className="text-gray-300 text-lg mb-4"> <div className="text-gray-300 text-base sm:text-lg mb-3 sm:mb-4">
No tickets were sold for this draw. No tickets were sold for this draw.
</div> </div>
<div className="text-bitcoin-orange text-xl font-semibold"> <div className="text-bitcoin-orange text-lg sm:text-xl font-semibold">
Next draw starting soon! Next draw starting soon!
</div> </div>
</div> </div>
<div className="mt-6 text-gray-400 text-sm animate-pulse"> <div className="mt-4 sm:mt-6 text-gray-400 text-xs sm:text-sm animate-pulse">
Click anywhere to continue Tap anywhere to continue
</div> </div>
</div> </div>
)} )}

View File

@@ -2,22 +2,22 @@ import Link from 'next/link';
export function Footer() { export function Footer() {
return ( return (
<footer className="bg-gray-900 border-t border-gray-800 py-8 mt-12"> <footer className="bg-gray-900 border-t border-gray-800 py-6 sm:py-8 mt-8 sm:mt-12 safe-bottom">
<div className="container mx-auto px-4"> <div className="container mx-auto px-4">
<div className="flex flex-col md:flex-row justify-between items-center"> <div className="flex flex-col md:flex-row justify-between items-center gap-4">
<div className="text-gray-400 text-sm mb-4 md:mb-0"> <div className="text-gray-400 text-xs sm:text-sm text-center md:text-left">
© 2025 Lightning Lottery. Powered by Bitcoin Lightning Network. © 2025 Lightning Lottery. Powered by Bitcoin Lightning Network.
</div> </div>
<div className="flex space-x-6"> <div className="flex space-x-6">
<Link <Link
href="/about" href="/about"
className="text-gray-400 hover:text-white transition-colors" className="text-gray-400 hover:text-white transition-colors text-sm py-2"
> >
About About
</Link> </Link>
<Link <Link
href="/past-wins" href="/past-wins"
className="text-gray-400 hover:text-white transition-colors" className="text-gray-400 hover:text-white transition-colors text-sm py-2"
> >
Past Winners Past Winners
</Link> </Link>

View File

@@ -31,34 +31,34 @@ export function JackpotCountdown({ scheduledAt, drawCompleted = false }: Jackpot
} }
return ( return (
<div className="flex space-x-4" role="timer" aria-live="polite"> <div className="flex space-x-2 sm:space-x-4" role="timer" aria-live="polite">
{countdown.days > 0 && ( {countdown.days > 0 && (
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="text-4xl md:text-5xl font-bold text-bitcoin-orange"> <div className="text-3xl sm:text-4xl md:text-5xl font-bold text-bitcoin-orange">
{countdown.days} {countdown.days}
</div> </div>
<div className="text-sm text-gray-400">days</div> <div className="text-xs sm:text-sm text-gray-400">days</div>
</div> </div>
)} )}
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="text-4xl md:text-5xl font-bold text-bitcoin-orange"> <div className="text-3xl sm:text-4xl md:text-5xl font-bold text-bitcoin-orange">
{countdown.hours.toString().padStart(2, '0')} {countdown.hours.toString().padStart(2, '0')}
</div> </div>
<div className="text-sm text-gray-400">hours</div> <div className="text-xs sm:text-sm text-gray-400">hours</div>
</div> </div>
<div className="text-4xl md:text-5xl font-bold text-gray-500">:</div> <div className="text-3xl sm:text-4xl md:text-5xl font-bold text-gray-500">:</div>
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="text-4xl md:text-5xl font-bold text-bitcoin-orange"> <div className="text-3xl sm:text-4xl md:text-5xl font-bold text-bitcoin-orange">
{countdown.minutes.toString().padStart(2, '0')} {countdown.minutes.toString().padStart(2, '0')}
</div> </div>
<div className="text-sm text-gray-400">minutes</div> <div className="text-xs sm:text-sm text-gray-400">min</div>
</div> </div>
<div className="text-4xl md:text-5xl font-bold text-gray-500">:</div> <div className="text-3xl sm:text-4xl md:text-5xl font-bold text-gray-500">:</div>
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="text-4xl md:text-5xl font-bold text-bitcoin-orange"> <div className="text-3xl sm:text-4xl md:text-5xl font-bold text-bitcoin-orange">
{countdown.seconds.toString().padStart(2, '0')} {countdown.seconds.toString().padStart(2, '0')}
</div> </div>
<div className="text-sm text-gray-400">seconds</div> <div className="text-xs sm:text-sm text-gray-400">sec</div>
</div> </div>
</div> </div>
); );

View File

@@ -1,4 +1,4 @@
import { formatSats, satsToBTC } from '@/lib/format'; import { formatSats } from '@/lib/format';
interface JackpotPotDisplayProps { interface JackpotPotDisplayProps {
potTotalSats: number; potTotalSats: number;
@@ -7,12 +7,9 @@ interface JackpotPotDisplayProps {
export function JackpotPotDisplay({ potTotalSats }: JackpotPotDisplayProps) { export function JackpotPotDisplay({ potTotalSats }: JackpotPotDisplayProps) {
return ( return (
<div className="text-center"> <div className="text-center">
<div className="text-5xl md:text-7xl font-bold text-bitcoin-orange mb-2"> <div className="text-4xl sm:text-5xl md:text-7xl font-bold text-bitcoin-orange">
{formatSats(potTotalSats)} {formatSats(potTotalSats)}
<span className="text-3xl md:text-4xl ml-2 text-gray-400">sats</span> <span className="text-2xl sm:text-3xl md:text-4xl ml-1 sm:ml-2 text-gray-400">sats</span>
</div>
<div className="text-xl md:text-2xl text-gray-400">
{satsToBTC(potTotalSats)} BTC
</div> </div>
</div> </div>
); );

View File

@@ -27,7 +27,7 @@ export function LightningInvoiceCard({
}; };
return ( return (
<div className="bg-white p-6 rounded-lg shadow-lg relative overflow-hidden"> <div className="bg-white p-4 sm:p-6 rounded-lg shadow-lg relative overflow-hidden">
{/* QR Code Container */} {/* QR Code Container */}
<div className="flex justify-center mb-4 relative"> <div className="flex justify-center mb-4 relative">
<div <div
@@ -37,9 +37,10 @@ export function LightningInvoiceCard({
> >
<QRCodeSVG <QRCodeSVG
value={paymentRequest.toUpperCase()} value={paymentRequest.toUpperCase()}
size={260} size={typeof window !== 'undefined' && window.innerWidth < 400 ? 200 : 260}
level="M" level="M"
includeMargin={true} includeMargin={true}
className="w-full max-w-[260px] h-auto"
/> />
</div> </div>
@@ -87,19 +88,19 @@ export function LightningInvoiceCard({
showPaidAnimation ? 'opacity-100' : 'opacity-0 h-0 overflow-hidden' showPaidAnimation ? 'opacity-100' : 'opacity-0 h-0 overflow-hidden'
}`} }`}
> >
<div className="text-green-600 font-bold text-lg">Payment Received!</div> <div className="text-green-600 font-bold text-base sm:text-lg">Payment Received!</div>
</div> </div>
{/* Amount */} {/* Amount */}
<div className="text-center mb-4"> <div className="text-center mb-4">
<div className="text-2xl font-bold text-gray-900"> <div className="text-xl sm:text-2xl font-bold text-gray-900">
{amountSats.toLocaleString()} sats {amountSats.toLocaleString()} sats
</div> </div>
</div> </div>
{/* Invoice */} {/* Invoice */}
<div className="mb-4"> <div className="mb-4">
<div className="bg-gray-100 p-3 rounded text-xs break-all text-gray-700 max-h-24 overflow-y-auto"> <div className="bg-gray-100 p-2 sm:p-3 rounded text-[10px] sm:text-xs break-all text-gray-700 max-h-20 sm:max-h-24 overflow-y-auto">
{paymentRequest} {paymentRequest}
</div> </div>
</div> </div>
@@ -108,10 +109,10 @@ export function LightningInvoiceCard({
<button <button
onClick={handleCopy} onClick={handleCopy}
disabled={showPaidAnimation} disabled={showPaidAnimation}
className={`w-full py-3 rounded-lg font-medium transition-all duration-300 ${ className={`w-full py-3 rounded-lg font-medium transition-all duration-300 text-sm sm:text-base ${
showPaidAnimation showPaidAnimation
? 'bg-green-500 text-white cursor-default' ? 'bg-green-500 text-white cursor-default'
: 'bg-bitcoin-orange hover:bg-orange-600 text-white' : 'bg-bitcoin-orange hover:bg-orange-600 active:bg-orange-700 text-white'
}`} }`}
> >
{showPaidAnimation ? '✓ Paid' : copied ? '✓ Copied!' : '📋 Copy Invoice'} {showPaidAnimation ? '✓ Paid' : copied ? '✓ Copied!' : '📋 Copy Invoice'}

View File

@@ -1,7 +1,7 @@
'use client'; 'use client';
import Link from 'next/link'; import Link from 'next/link';
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { useAppSelector } from '@/store/hooks'; import { useAppSelector } from '@/store/hooks';
import { NostrLoginButton } from './NostrLoginButton'; import { NostrLoginButton } from './NostrLoginButton';
import { shortNpub, hexToNpub } from '@/lib/nostr'; import { shortNpub, hexToNpub } from '@/lib/nostr';
@@ -9,9 +9,87 @@ import STRINGS from '@/constants/strings';
export function TopBar() { export function TopBar() {
const user = useAppSelector((state) => state.user); const user = useAppSelector((state) => state.user);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
// Close menu when clicking outside or pressing escape
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') setMobileMenuOpen(false);
};
if (mobileMenuOpen) {
document.addEventListener('keydown', handleEscape);
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.removeEventListener('keydown', handleEscape);
document.body.style.overflow = '';
};
}, [mobileMenuOpen]);
const closeMenu = () => setMobileMenuOpen(false);
const NavLinks = ({ mobile = false }: { mobile?: boolean }) => (
<>
<Link
href="/"
onClick={closeMenu}
className={`${
mobile
? 'block py-3 px-4 text-lg text-gray-200 hover:bg-gray-800 rounded-lg transition-colors'
: 'text-gray-300 hover:text-white transition-colors'
}`}
>
Home
</Link>
<Link
href="/buy"
onClick={closeMenu}
className={`${
mobile
? 'block py-3 px-4 text-lg text-gray-200 hover:bg-gray-800 rounded-lg transition-colors'
: 'text-gray-300 hover:text-white transition-colors'
}`}
>
Buy Tickets
</Link>
<Link
href="/past-wins"
onClick={closeMenu}
className={`${
mobile
? 'block py-3 px-4 text-lg text-gray-200 hover:bg-gray-800 rounded-lg transition-colors'
: 'text-gray-300 hover:text-white transition-colors'
}`}
>
Past Winners
</Link>
{user.authenticated ? (
<Link
href="/dashboard"
onClick={closeMenu}
className={`${
mobile
? 'block py-3 px-4 text-lg text-gray-200 hover:bg-gray-800 rounded-lg transition-colors'
: 'text-gray-300 hover:text-white transition-colors'
}`}
>
Dashboard
</Link>
) : (
<div className={mobile ? 'py-3 px-4' : ''} onClick={closeMenu}>
<NostrLoginButton />
</div>
)}
</>
);
return ( return (
<nav className="bg-gray-900 border-b border-gray-800"> <nav className="bg-gray-900 border-b border-gray-800 sticky top-0 z-40">
<div className="container mx-auto px-4"> <div className="container mx-auto px-4">
<div className="flex items-center justify-between h-16"> <div className="flex items-center justify-between h-16">
{/* Logo */} {/* Logo */}
@@ -22,41 +100,106 @@ export function TopBar() {
</span> </span>
</Link> </Link>
{/* Navigation */} {/* Desktop Navigation */}
<div className="flex items-center space-x-6"> <div className="hidden md:flex items-center space-x-6">
<Link <NavLinks />
href="/" </div>
className="text-gray-300 hover:text-white transition-colors"
> {/* Mobile Menu Button */}
Home <button
</Link> type="button"
<Link className="md:hidden p-2 text-gray-400 hover:text-white focus:outline-none focus:ring-2 focus:ring-bitcoin-orange rounded-lg"
href="/buy" onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="text-gray-300 hover:text-white transition-colors" aria-label={mobileMenuOpen ? 'Close menu' : 'Open menu'}
> aria-expanded={mobileMenuOpen}
Buy Tickets >
</Link> {mobileMenuOpen ? (
<Link // X icon
href="/past-wins" <svg
className="text-gray-300 hover:text-white transition-colors" className="w-6 h-6"
> fill="none"
Past Winners stroke="currentColor"
</Link> viewBox="0 0 24 24"
{user.authenticated ? (
<Link
href="/dashboard"
className="text-gray-300 hover:text-white transition-colors"
> >
Dashboard <path
</Link> strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
) : ( ) : (
<NostrLoginButton /> // Hamburger icon
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
)} )}
</button>
</div>
</div>
{/* Mobile Menu Overlay */}
{mobileMenuOpen && (
<div
className="fixed inset-0 bg-black/60 z-40 md:hidden"
onClick={closeMenu}
aria-hidden="true"
/>
)}
{/* Mobile Menu Panel */}
<div
className={`fixed top-0 right-0 h-full w-72 bg-gray-900 border-l border-gray-800 z-50 transform transition-transform duration-300 ease-in-out md:hidden ${
mobileMenuOpen ? 'translate-x-0' : 'translate-x-full'
}`}
>
{/* Mobile Menu Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-800">
<span className="text-lg font-semibold text-white">Menu</span>
<button
type="button"
className="p-2 text-gray-400 hover:text-white focus:outline-none focus:ring-2 focus:ring-bitcoin-orange rounded-lg"
onClick={closeMenu}
aria-label="Close menu"
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
{/* Mobile Menu Links */}
<div className="p-4 space-y-2">
<NavLinks mobile />
</div>
{/* Mobile Menu Footer */}
<div className="absolute bottom-0 left-0 right-0 p-4 border-t border-gray-800">
<div className="text-center text-gray-500 text-sm">
Lightning Lotto
</div> </div>
</div> </div>
</div> </div>
</nav> </nav>
); );
} }