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

View File

@@ -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>

View File

@@ -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,21 +75,68 @@ body {
}
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
/* Custom scrollbar (desktop) */
@media (hover: hover) and (pointer: fine) {
::-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 {
background: #0b0b0b;
/* 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;
}
}
::-webkit-scrollbar-thumb {
background: #333;
border-radius: 4px;
/* Prevent iOS bounce/pull-to-refresh on certain elements */
.no-bounce {
overscroll-behavior: none;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
/* 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;
}
}

View File

@@ -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({

View File

@@ -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>

View File

@@ -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,28 +183,30 @@ 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>
<JackpotCountdown scheduledAt={cycle.scheduled_at} />
<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>
@@ -212,14 +214,14 @@ export default function TicketStatusPage() {
{/* 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>
)}