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:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -88,3 +88,5 @@ ehthumbs.db
|
||||
*.key
|
||||
secrets/
|
||||
|
||||
# Setup/deployment configs (contains server-specific settings)
|
||||
setup/
|
||||
|
||||
@@ -65,16 +65,28 @@ export async function getNextJackpot(req: Request, res: Response) {
|
||||
|
||||
const lottery = lotteryResult.rows[0];
|
||||
|
||||
// Get next cycle
|
||||
const cycleResult = await db.query<JackpotCycle>(
|
||||
// Get next cycle - first try to find one that hasn't drawn yet
|
||||
let cycleResult = await db.query<JackpotCycle>(
|
||||
`SELECT * FROM jackpot_cycles
|
||||
WHERE lottery_id = $1
|
||||
AND status IN ('scheduled', 'sales_open')
|
||||
AND status IN ('scheduled', 'sales_open', 'drawing')
|
||||
ORDER BY scheduled_at ASC
|
||||
LIMIT 1`,
|
||||
[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) {
|
||||
return res.status(503).json({
|
||||
|
||||
@@ -222,15 +222,15 @@ export default function BuyPage() {
|
||||
const totalCost = ticketPriceSats * tickets;
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<h1 className="text-3xl md:text-4xl font-bold mb-8 text-center text-white">
|
||||
<div className="max-w-2xl 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.buy.title}
|
||||
</h1>
|
||||
|
||||
{!invoice ? (
|
||||
/* Purchase Form */
|
||||
<div className="bg-gray-900 rounded-xl p-8 border border-gray-800">
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="bg-gray-900 rounded-xl p-5 sm:p-8 border border-gray-800">
|
||||
<form onSubmit={handleSubmit} className="space-y-5 sm:space-y-6">
|
||||
{/* Lightning Address */}
|
||||
<div>
|
||||
<label className="block text-gray-300 mb-2 font-medium">
|
||||
@@ -321,7 +321,7 @@ export default function BuyPage() {
|
||||
<button
|
||||
type="submit"
|
||||
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}
|
||||
</button>
|
||||
|
||||
@@ -10,8 +10,17 @@
|
||||
body {
|
||||
color: rgb(var(--foreground-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 {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
@@ -20,6 +29,39 @@ body {
|
||||
.animate-fade-in {
|
||||
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 {
|
||||
@@ -33,7 +75,8 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
/* Custom scrollbar (desktop) */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
@@ -50,4 +93,50 @@ body {
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile-specific styles */
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
/* Prevent iOS bounce/pull-to-refresh on certain elements */
|
||||
.no-bounce {
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
/* Focus visible only for keyboard navigation */
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
*:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
nav, footer, button {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body {
|
||||
background: white;
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Metadata } from 'next';
|
||||
import type { Metadata, Viewport } from 'next';
|
||||
import { Inter } from 'next/font/google';
|
||||
import './globals.css';
|
||||
import { Providers } from './providers';
|
||||
@@ -7,9 +7,25 @@ import { Footer } from '@/components/Footer';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
export const viewport: Viewport = {
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
maximumScale: 5,
|
||||
userScalable: true,
|
||||
themeColor: '#0b0b0b',
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Lightning Lottery - Win Bitcoin',
|
||||
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({
|
||||
|
||||
@@ -246,20 +246,20 @@ export default function HomePage() {
|
||||
)}
|
||||
|
||||
{/* Hero Section */}
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl md:text-6xl font-bold mb-4 text-white">
|
||||
<div className="text-center mb-8 sm:mb-12">
|
||||
<h1 className="text-3xl sm:text-4xl md:text-6xl font-bold mb-3 sm:mb-4 text-white">
|
||||
{STRINGS.app.title}
|
||||
</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>
|
||||
|
||||
{/* Recent Winner Banner - Only shown for 60 seconds after draw */}
|
||||
{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 */}
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -267,16 +267,16 @@ export default function HomePage() {
|
||||
</svg>
|
||||
</button>
|
||||
<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
|
||||
</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'}
|
||||
</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
|
||||
</div>
|
||||
<div className="text-gray-400 text-sm">
|
||||
<div className="text-gray-400 text-xs sm:text-sm">
|
||||
Ticket #{recentWinner.winning_ticket_serial.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
@@ -284,22 +284,22 @@ export default function HomePage() {
|
||||
)}
|
||||
|
||||
{/* Current Jackpot Card */}
|
||||
<div className="bg-gray-900 rounded-2xl p-8 md:p-12 mb-8 border border-gray-800">
|
||||
<h2 className="text-2xl font-semibold text-center mb-6 text-gray-300">
|
||||
<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-xl sm:text-2xl font-semibold text-center mb-4 sm:mb-6 text-gray-300">
|
||||
{STRINGS.home.currentJackpot}
|
||||
</h2>
|
||||
|
||||
{/* Pot Display */}
|
||||
<div className="mb-8">
|
||||
<div className="mb-6 sm:mb-8">
|
||||
<JackpotPotDisplay potTotalSats={jackpot.cycle.pot_total_sats} />
|
||||
</div>
|
||||
|
||||
{/* Countdown */}
|
||||
<div className="mb-8">
|
||||
<div className="text-center text-gray-400 mb-4">
|
||||
<div className="mb-6 sm:mb-8">
|
||||
<div className="text-center text-gray-400 mb-3 sm:mb-4 text-sm sm:text-base">
|
||||
{STRINGS.home.drawIn}
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<div className="flex justify-center overflow-x-auto scrollbar-hide">
|
||||
<JackpotCountdown
|
||||
scheduledAt={jackpot.cycle.scheduled_at}
|
||||
drawCompleted={awaitingNextCycle || drawJustCompleted}
|
||||
@@ -308,7 +308,7 @@ export default function HomePage() {
|
||||
</div>
|
||||
|
||||
{/* 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
|
||||
</div>
|
||||
|
||||
@@ -317,7 +317,7 @@ export default function HomePage() {
|
||||
<div className="flex justify-center">
|
||||
<Link
|
||||
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}
|
||||
</Link>
|
||||
@@ -326,8 +326,8 @@ export default function HomePage() {
|
||||
</div>
|
||||
|
||||
{/* Check Ticket Section */}
|
||||
<div className="bg-gray-900 rounded-2xl p-8 border border-gray-800">
|
||||
<h3 className="text-xl font-semibold text-center mb-4 text-gray-300">
|
||||
<div className="bg-gray-900 rounded-xl sm:rounded-2xl p-5 sm:p-8 border border-gray-800">
|
||||
<h3 className="text-lg sm:text-xl font-semibold text-center mb-4 text-gray-300">
|
||||
{STRINGS.home.checkTicket}
|
||||
</h3>
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
@@ -336,12 +336,12 @@ export default function HomePage() {
|
||||
value={ticketId}
|
||||
onChange={(e) => setTicketId(e.target.value)}
|
||||
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()}
|
||||
/>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
@@ -349,25 +349,25 @@ export default function HomePage() {
|
||||
</div>
|
||||
|
||||
{/* Info Section */}
|
||||
<div className="mt-12 grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="bg-gray-900 p-6 rounded-lg border border-gray-800">
|
||||
<div className="text-4xl mb-3">⚡</div>
|
||||
<h4 className="text-lg font-semibold mb-2 text-white">Instant</h4>
|
||||
<p className="text-gray-400 text-sm">
|
||||
<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-5 sm:p-6 rounded-lg border border-gray-800">
|
||||
<div className="text-3xl sm:text-4xl mb-2 sm:mb-3">⚡</div>
|
||||
<h4 className="text-base sm:text-lg font-semibold mb-1 sm:mb-2 text-white">Instant</h4>
|
||||
<p className="text-gray-400 text-xs sm:text-sm">
|
||||
Lightning-fast ticket purchases and payouts
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-900 p-6 rounded-lg border border-gray-800">
|
||||
<div className="text-4xl mb-3">🔒</div>
|
||||
<h4 className="text-lg font-semibold mb-2 text-white">Secure</h4>
|
||||
<p className="text-gray-400 text-sm">
|
||||
<div className="bg-gray-900 p-5 sm:p-6 rounded-lg border border-gray-800">
|
||||
<div className="text-3xl sm:text-4xl mb-2 sm:mb-3">🔒</div>
|
||||
<h4 className="text-base sm:text-lg font-semibold mb-1 sm:mb-2 text-white">Secure</h4>
|
||||
<p className="text-gray-400 text-xs sm:text-sm">
|
||||
Cryptographically secure random number generation
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-900 p-6 rounded-lg border border-gray-800">
|
||||
<div className="text-4xl mb-3">🎯</div>
|
||||
<h4 className="text-lg font-semibold mb-2 text-white">Fair</h4>
|
||||
<p className="text-gray-400 text-sm">
|
||||
<div className="bg-gray-900 p-5 sm:p-6 rounded-lg border border-gray-800">
|
||||
<div className="text-3xl sm:text-4xl mb-2 sm:mb-3">🎯</div>
|
||||
<h4 className="text-base sm:text-lg font-semibold mb-1 sm:mb-2 text-white">Fair</h4>
|
||||
<p className="text-gray-400 text-xs sm:text-sm">
|
||||
Transparent draws with verifiable results
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -96,30 +96,30 @@ export default function TicketStatusPage() {
|
||||
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">
|
||||
<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-6 mb-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="text-3xl">🔖</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-white mb-2">Save This Link!</h3>
|
||||
<p className="text-gray-300 text-sm mb-3">
|
||||
<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 sm:flex-row 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="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={`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
|
||||
? '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 ? (
|
||||
@@ -144,11 +144,11 @@ export default function TicketStatusPage() {
|
||||
</div>
|
||||
|
||||
{/* 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 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">{purchase.id}</div>
|
||||
<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>
|
||||
@@ -167,15 +167,15 @@ export default function TicketStatusPage() {
|
||||
|
||||
{/* 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">
|
||||
<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-6 mb-6 border border-gray-800">
|
||||
<h2 className="text-xl font-semibold mb-4 text-gray-300">
|
||||
<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} />
|
||||
@@ -183,43 +183,45 @@ export default function TicketStatusPage() {
|
||||
)}
|
||||
|
||||
{/* 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">
|
||||
<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-4">
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
<div>
|
||||
<span className="text-gray-400">Draw Time:</span>
|
||||
<div className="text-white">{formatDateTime(cycle.scheduled_at)}</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">Current Pot:</span>
|
||||
<div className="text-2xl font-bold text-bitcoin-orange">
|
||||
<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 block mb-2">Time Until Draw:</span>
|
||||
<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-6 border border-gray-800">
|
||||
<h2 className="text-xl font-semibold mb-4 text-gray-300">
|
||||
<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-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}
|
||||
</div>
|
||||
{result.payout && (
|
||||
@@ -231,10 +233,10 @@ export default function TicketStatusPage() {
|
||||
</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>
|
||||
<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">
|
||||
<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>
|
||||
)}
|
||||
|
||||
@@ -147,17 +147,17 @@ export function DrawAnimation({
|
||||
</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 */}
|
||||
{(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...
|
||||
</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="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-xs sm:text-sm mb-2">Ticket Number</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' : ''
|
||||
}`}
|
||||
>
|
||||
@@ -170,26 +170,26 @@ export function DrawAnimation({
|
||||
{/* Winner Phase */}
|
||||
{phase === 'winner' && hasWinner && (
|
||||
<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">
|
||||
<div className="text-3xl sm:text-4xl mb-3 sm:mb-4">🎉🏆🎉</div>
|
||||
<div className="text-2xl sm:text-3xl md:text-4xl font-bold text-yellow-400 mb-4 sm: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">
|
||||
<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-xs sm:text-sm mb-1">Winner</div>
|
||||
<div className="text-2xl sm:text-3xl md:text-4xl font-bold text-white mb-3 sm:mb-4 break-all">
|
||||
{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">
|
||||
<div className="text-gray-300 text-xs sm:text-sm mb-1">Winning Ticket</div>
|
||||
<div className="text-xl sm:text-2xl font-mono text-bitcoin-orange mb-3 sm: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">
|
||||
<div className="text-gray-300 text-xs sm:text-sm mb-1">Prize</div>
|
||||
<div className="text-3xl sm: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 className="mt-4 sm:mt-6 text-gray-400 text-xs sm:text-sm animate-pulse">
|
||||
Tap anywhere to continue
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -197,20 +197,20 @@ export function DrawAnimation({
|
||||
{/* No Winner Phase (no tickets sold) */}
|
||||
{phase === 'no-winner' && (
|
||||
<div className="animate-winner-reveal">
|
||||
<div className="text-4xl mb-4">😔</div>
|
||||
<div className="text-2xl md:text-3xl font-bold text-gray-400 mb-6">
|
||||
<div className="text-3xl sm:text-4xl mb-3 sm:mb-4">😔</div>
|
||||
<div className="text-xl sm:text-2xl md:text-3xl font-bold text-gray-400 mb-4 sm:mb-6">
|
||||
No Tickets This Round
|
||||
</div>
|
||||
<div className="bg-gray-900 rounded-2xl p-8 border-2 border-gray-600 shadow-2xl">
|
||||
<div className="text-gray-300 text-lg mb-4">
|
||||
<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-base sm:text-lg mb-3 sm:mb-4">
|
||||
No tickets were sold for this draw.
|
||||
</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!
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 text-gray-400 text-sm animate-pulse">
|
||||
Click anywhere to continue
|
||||
<div className="mt-4 sm:mt-6 text-gray-400 text-xs sm:text-sm animate-pulse">
|
||||
Tap anywhere to continue
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2,22 +2,22 @@ import Link from 'next/link';
|
||||
|
||||
export function Footer() {
|
||||
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="flex flex-col md:flex-row justify-between items-center">
|
||||
<div className="text-gray-400 text-sm mb-4 md:mb-0">
|
||||
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
<div className="text-gray-400 text-xs sm:text-sm text-center md:text-left">
|
||||
© 2025 Lightning Lottery. Powered by Bitcoin Lightning Network.
|
||||
</div>
|
||||
<div className="flex space-x-6">
|
||||
<Link
|
||||
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
|
||||
</Link>
|
||||
<Link
|
||||
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
|
||||
</Link>
|
||||
|
||||
@@ -31,34 +31,34 @@ export function JackpotCountdown({ scheduledAt, drawCompleted = false }: Jackpot
|
||||
}
|
||||
|
||||
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 && (
|
||||
<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}
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">days</div>
|
||||
<div className="text-xs sm:text-sm text-gray-400">days</div>
|
||||
</div>
|
||||
)}
|
||||
<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')}
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">hours</div>
|
||||
<div className="text-xs sm:text-sm text-gray-400">hours</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="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')}
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">minutes</div>
|
||||
<div className="text-xs sm:text-sm text-gray-400">min</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="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')}
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">seconds</div>
|
||||
<div className="text-xs sm:text-sm text-gray-400">sec</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { formatSats, satsToBTC } from '@/lib/format';
|
||||
import { formatSats } from '@/lib/format';
|
||||
|
||||
interface JackpotPotDisplayProps {
|
||||
potTotalSats: number;
|
||||
@@ -7,12 +7,9 @@ interface JackpotPotDisplayProps {
|
||||
export function JackpotPotDisplay({ potTotalSats }: JackpotPotDisplayProps) {
|
||||
return (
|
||||
<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)}
|
||||
<span className="text-3xl md:text-4xl ml-2 text-gray-400">sats</span>
|
||||
</div>
|
||||
<div className="text-xl md:text-2xl text-gray-400">
|
||||
≈ {satsToBTC(potTotalSats)} BTC
|
||||
<span className="text-2xl sm:text-3xl md:text-4xl ml-1 sm:ml-2 text-gray-400">sats</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -27,7 +27,7 @@ export function LightningInvoiceCard({
|
||||
};
|
||||
|
||||
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 */}
|
||||
<div className="flex justify-center mb-4 relative">
|
||||
<div
|
||||
@@ -37,9 +37,10 @@ export function LightningInvoiceCard({
|
||||
>
|
||||
<QRCodeSVG
|
||||
value={paymentRequest.toUpperCase()}
|
||||
size={260}
|
||||
size={typeof window !== 'undefined' && window.innerWidth < 400 ? 200 : 260}
|
||||
level="M"
|
||||
includeMargin={true}
|
||||
className="w-full max-w-[260px] h-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -87,19 +88,19 @@ export function LightningInvoiceCard({
|
||||
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>
|
||||
|
||||
{/* Amount */}
|
||||
<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
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Invoice */}
|
||||
<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}
|
||||
</div>
|
||||
</div>
|
||||
@@ -108,10 +109,10 @@ export function LightningInvoiceCard({
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
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
|
||||
? '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'}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useAppSelector } from '@/store/hooks';
|
||||
import { NostrLoginButton } from './NostrLoginButton';
|
||||
import { shortNpub, hexToNpub } from '@/lib/nostr';
|
||||
@@ -9,9 +9,87 @@ import STRINGS from '@/constants/strings';
|
||||
|
||||
export function TopBar() {
|
||||
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 (
|
||||
<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="flex items-center justify-between h-16">
|
||||
{/* Logo */}
|
||||
@@ -22,41 +100,106 @@ export function TopBar() {
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex items-center space-x-6">
|
||||
<Link
|
||||
href="/"
|
||||
className="text-gray-300 hover:text-white transition-colors"
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
<Link
|
||||
href="/buy"
|
||||
className="text-gray-300 hover:text-white transition-colors"
|
||||
>
|
||||
Buy Tickets
|
||||
</Link>
|
||||
<Link
|
||||
href="/past-wins"
|
||||
className="text-gray-300 hover:text-white transition-colors"
|
||||
>
|
||||
Past Winners
|
||||
</Link>
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden md:flex items-center space-x-6">
|
||||
<NavLinks />
|
||||
</div>
|
||||
|
||||
{user.authenticated ? (
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="text-gray-300 hover:text-white transition-colors"
|
||||
{/* Mobile Menu Button */}
|
||||
<button
|
||||
type="button"
|
||||
className="md:hidden p-2 text-gray-400 hover:text-white focus:outline-none focus:ring-2 focus:ring-bitcoin-orange rounded-lg"
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
aria-label={mobileMenuOpen ? 'Close menu' : 'Open menu'}
|
||||
aria-expanded={mobileMenuOpen}
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
{mobileMenuOpen ? (
|
||||
// X icon
|
||||
<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>
|
||||
) : (
|
||||
<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>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user