Improve mobile/Android UI responsiveness
- Add xs (375px) breakpoint to Tailwind config for small phone targeting - Fix main content bottom padding to account for safe-area-inset-bottom - Redesign mobile bottom nav: active dot indicator, proper safe-area padding, flex-1 tabs that scale to any width, tighter text on xs screens - Fix hero section: smaller padding/text on tiny phones (320-375px), horizontal badge wrapping, full-width buttons stack below xs - Add CSS improvements: overscroll-behavior, -webkit-text-size-adjust, input font-size fix for iOS zoom prevention, scroll-x helper class, touch-friendly min tap target sizes Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
1441855eb1
commit
aac2b7b9a2
4 changed files with 140 additions and 94 deletions
|
|
@ -236,88 +236,97 @@ function MobileNav({
|
||||||
userName?: string;
|
userName?: string;
|
||||||
t: (key: string) => string;
|
t: (key: string) => string;
|
||||||
}) {
|
}) {
|
||||||
|
// Decide which tabs to show — max 5 on small screens
|
||||||
|
// Logged in: Home, Map, Lab, Events, Profile (wallet in profile dropdown)
|
||||||
|
// Logged out: Home, Map, Lab, Events, Login
|
||||||
|
const tabs = [
|
||||||
|
...navItemKeys,
|
||||||
|
...(isLoggedIn
|
||||||
|
? [{ href: '/wallet', labelKey: 'nav.wallet', icon: Wallet }]
|
||||||
|
: []),
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="fixed bottom-0 left-0 right-0 z-50 md:hidden safe-area-bottom">
|
<nav className="fixed bottom-0 left-0 right-0 z-50 md:hidden">
|
||||||
<div className="glass border-t border-border safe-area-left safe-area-right">
|
{/* Nav bar */}
|
||||||
<div className="flex items-center justify-around px-1 py-2">
|
<div
|
||||||
{navItemKeys.map((item) => (
|
className="glass border-t border-white/10"
|
||||||
<Link
|
style={{ paddingBottom: 'env(safe-area-inset-bottom, 0px)', paddingLeft: 'env(safe-area-inset-left, 0px)', paddingRight: 'env(safe-area-inset-right, 0px)' }}
|
||||||
key={item.href}
|
>
|
||||||
to={item.href}
|
<div className="flex items-stretch justify-around px-1 pt-1 pb-1">
|
||||||
className={cn(
|
{tabs.map((item) => {
|
||||||
'flex flex-col items-center justify-center gap-0.5 py-2 px-2 rounded-xl transition-all duration-200 min-w-[48px] min-h-[48px] touch-manipulation active:scale-95',
|
const active = isActive(item.href);
|
||||||
isActive(item.href)
|
const isWallet = item.href === '/wallet';
|
||||||
? 'text-primary'
|
return (
|
||||||
: 'text-muted-foreground'
|
<Link
|
||||||
)}
|
key={item.href}
|
||||||
>
|
to={item.href}
|
||||||
<item.icon className={cn(
|
className={cn(
|
||||||
'h-5 w-5 transition-transform',
|
'relative flex flex-col items-center justify-center gap-0.5 py-1.5 px-1 rounded-xl transition-all duration-150 flex-1 touch-manipulation active:scale-90',
|
||||||
isActive(item.href) && 'scale-110'
|
active
|
||||||
)} />
|
? isWallet ? 'text-orange-400' : 'text-[#ff0040]'
|
||||||
<span className="text-[10px] font-medium leading-tight">{t(item.labelKey)}</span>
|
: 'text-gray-500'
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
{/* Wallet in mobile nav */}
|
|
||||||
{isLoggedIn && (
|
|
||||||
<Link
|
|
||||||
to="/wallet"
|
|
||||||
className={cn(
|
|
||||||
'flex flex-col items-center justify-center gap-0.5 py-2 px-2 rounded-xl transition-all duration-200 min-w-[48px] min-h-[48px] relative touch-manipulation active:scale-95',
|
|
||||||
isActive('/wallet')
|
|
||||||
? 'text-orange-500'
|
|
||||||
: 'text-muted-foreground'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="relative">
|
|
||||||
<Wallet className={cn(
|
|
||||||
'h-5 w-5 transition-transform',
|
|
||||||
isActive('/wallet') && 'scale-110'
|
|
||||||
)} />
|
|
||||||
{cashuBalance > 0 && (
|
|
||||||
<span className="absolute -top-1 -right-2 min-w-[16px] h-4 px-1 text-[8px] font-bold bg-orange-500 text-white rounded-full flex items-center justify-center">
|
|
||||||
{formatSatsCompact(cashuBalance)}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
>
|
||||||
<span className="text-[10px] font-medium leading-tight">{t('nav.wallet')}</span>
|
{/* Active indicator dot */}
|
||||||
</Link>
|
{active && (
|
||||||
)}
|
<span className={cn(
|
||||||
{/* Profile/Login in mobile nav */}
|
'absolute top-0 left-1/2 -translate-x-1/2 w-1 h-1 rounded-full',
|
||||||
|
isWallet ? 'bg-orange-400' : 'bg-[#ff0040]'
|
||||||
|
)} />
|
||||||
|
)}
|
||||||
|
{isWallet ? (
|
||||||
|
<div className="relative">
|
||||||
|
<Wallet className={cn('h-[22px] w-[22px] transition-transform', active && 'scale-110')} />
|
||||||
|
{cashuBalance > 0 && (
|
||||||
|
<span className="absolute -top-1.5 -right-2.5 min-w-[16px] h-4 px-1 text-[8px] font-bold bg-orange-500 text-white rounded-full flex items-center justify-center leading-none">
|
||||||
|
{formatSatsCompact(cashuBalance)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<item.icon className={cn('h-[22px] w-[22px] transition-transform', active && 'scale-110')} />
|
||||||
|
)}
|
||||||
|
<span className="text-[9px] xs:text-[10px] font-semibold leading-tight tracking-wide uppercase">
|
||||||
|
{t(item.labelKey)}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Profile / Login tab */}
|
||||||
<Link
|
<Link
|
||||||
to={isLoggedIn ? "/settings" : "/settings"}
|
to="/settings"
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-col items-center justify-center gap-0.5 py-2 px-2 rounded-xl transition-all duration-200 min-w-[48px] min-h-[48px] relative touch-manipulation active:scale-95',
|
'relative flex flex-col items-center justify-center gap-0.5 py-1.5 px-1 rounded-xl transition-all duration-150 flex-1 touch-manipulation active:scale-90',
|
||||||
isActive('/settings')
|
isActive('/settings') ? 'text-[#ff0040]' : 'text-gray-500'
|
||||||
? 'text-primary'
|
|
||||||
: 'text-muted-foreground'
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{isActive('/settings') && (
|
||||||
|
<span className="absolute top-0 left-1/2 -translate-x-1/2 w-1 h-1 rounded-full bg-[#ff0040]" />
|
||||||
|
)}
|
||||||
{isLoggedIn && userPubkey ? (
|
{isLoggedIn && userPubkey ? (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Avatar className={cn(
|
<Avatar className={cn(
|
||||||
'h-6 w-6 border-2 transition-transform',
|
'h-[22px] w-[22px] border-2 transition-transform',
|
||||||
isActive('/settings') ? 'border-primary scale-110' : 'border-gray-600'
|
isActive('/settings') ? 'border-[#ff0040] scale-110' : 'border-gray-600'
|
||||||
)}>
|
)}>
|
||||||
<AvatarImage src={userAvatar} alt={userName} />
|
<AvatarImage src={userAvatar} alt={userName} />
|
||||||
<AvatarFallback className="bg-[#ff0040]/20 text-[#ff0040] text-[8px] font-bold">
|
<AvatarFallback className="bg-[#ff0040]/20 text-[#ff0040] text-[8px] font-bold">
|
||||||
{(userName || '??').slice(0, 2).toUpperCase()}
|
{(userName || '??').slice(0, 2).toUpperCase()}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
{/* Green online indicator dot */}
|
<span className="absolute -bottom-0.5 -right-0.5 w-2 h-2 bg-green-500 border border-black rounded-full" />
|
||||||
<span className="absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 bg-green-500 border-2 border-black rounded-full" />
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<User className="h-5 w-5" />
|
<User className="h-[22px] w-[22px]" />
|
||||||
)}
|
)}
|
||||||
<span className="text-[10px] font-medium leading-tight">
|
<span className="text-[9px] xs:text-[10px] font-semibold leading-tight tracking-wide uppercase">
|
||||||
{isLoggedIn ? 'Profile' : 'Log In'}
|
{isLoggedIn ? 'Me' : 'Log In'}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Safe area spacer for notched devices */}
|
|
||||||
<div className="h-safe-area-bottom bg-background" />
|
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -741,8 +750,8 @@ export function Layout({ children }: LayoutProps) {
|
||||||
{/* Backup Reminder */}
|
{/* Backup Reminder */}
|
||||||
<BackupReminder />
|
<BackupReminder />
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content — extra bottom padding on mobile so the fixed bottom nav never overlaps */}
|
||||||
<main className="pb-20 md:pb-6">
|
<main className="pb-[calc(5rem+env(safe-area-inset-bottom,0px))] md:pb-6">
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -347,25 +347,54 @@
|
||||||
html, body {
|
html, body {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
|
/* Prevent elastic overscroll on the root — pages handle their own scroll */
|
||||||
|
overscroll-behavior: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Improve touch targets for mobile */
|
/* Smooth momentum scrolling on iOS */
|
||||||
|
* {
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Better tap highlight on mobile — subtle red glow */
|
||||||
|
a, button {
|
||||||
|
-webkit-tap-highlight-color: rgba(255, 0, 64, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Improve touch targets for mobile — WCAG minimum 44×44px */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
button, a, input, select, textarea {
|
button, [role="button"] {
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
}
|
min-width: 44px;
|
||||||
|
|
||||||
/* Better tap highlight on mobile */
|
|
||||||
a, button {
|
|
||||||
-webkit-tap-highlight-color: rgba(255, 0, 64, 0.1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fix input zoom on iOS */
|
/* Fix input zoom on iOS — font-size must be ≥16px to prevent auto-zoom */
|
||||||
@media screen and (max-width: 768px) {
|
input, select, textarea {
|
||||||
input, select, textarea {
|
font-size: max(16px, 1em);
|
||||||
font-size: 16px !important;
|
}
|
||||||
}
|
|
||||||
|
/* Prevent text size adjustments after orientation changes on iOS */
|
||||||
|
html {
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Better scrolling containers — prevent pull-to-refresh on inner scrollable areas */
|
||||||
|
.scroll-container {
|
||||||
|
overscroll-behavior-y: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Touch-friendly horizontal scroll rows (e.g. filter chips) */
|
||||||
|
.scroll-x {
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
overscroll-behavior-x: contain;
|
||||||
|
scroll-snap-type: x proximity;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
.scroll-x > * {
|
||||||
|
scroll-snap-align: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide scrollbar utility - useful for horizontal scrolling filters */
|
/* Hide scrollbar utility - useful for horizontal scrolling filters */
|
||||||
|
|
|
||||||
|
|
@ -40,62 +40,62 @@ function HeroSection() {
|
||||||
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] bg-[#ccff00] opacity-15 rounded-full blur-[120px]" />
|
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] bg-[#ccff00] opacity-15 rounded-full blur-[120px]" />
|
||||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[400px] h-[400px] bg-[#ff6600] opacity-10 rounded-full blur-[100px]" />
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[400px] h-[400px] bg-[#ff6600] opacity-10 rounded-full blur-[100px]" />
|
||||||
|
|
||||||
<div className="relative container px-4 sm:px-6 md:px-8 py-16 sm:py-20 md:py-28">
|
<div className="relative container px-3 xs:px-4 sm:px-6 md:px-8 py-10 xs:py-14 sm:py-20 md:py-28">
|
||||||
<div className="max-w-4xl mx-auto text-center">
|
<div className="max-w-4xl mx-auto text-center">
|
||||||
<div className="mb-8">
|
<div className="mb-6 xs:mb-8">
|
||||||
<div className="inline-block px-6 py-2 rounded-full border-2 border-[#ff0040]/50 bg-black/50 backdrop-blur-sm mb-6">
|
<div className="inline-flex items-center px-3 xs:px-6 py-1.5 xs:py-2 rounded-full border-2 border-[#ff0040]/50 bg-black/50 backdrop-blur-sm mb-4 xs:mb-6">
|
||||||
<span className="text-[#ff0040] font-bold uppercase tracking-wider text-sm">
|
<span className="text-[#ff0040] font-bold uppercase tracking-wider text-xs xs:text-sm">
|
||||||
<Sparkles className="inline h-4 w-4 mr-2" />
|
<Sparkles className="inline h-3 w-3 xs:h-4 xs:w-4 mr-1 xs:mr-2" />
|
||||||
{t('home.tagline')}
|
{t('home.tagline')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="text-4xl sm:text-5xl md:text-7xl lg:text-8xl font-black uppercase mb-6 leading-[0.9]" style={{
|
<h1 className="text-3xl xs:text-4xl sm:text-5xl md:text-7xl lg:text-8xl font-black uppercase mb-4 xs:mb-6 leading-[0.9]" style={{
|
||||||
fontFamily: 'Impact, "Arial Black", sans-serif',
|
fontFamily: 'Impact, "Arial Black", sans-serif',
|
||||||
letterSpacing: '0.05em'
|
letterSpacing: '0.05em'
|
||||||
}}>
|
}}>
|
||||||
<span className="block gradient-text">{t('home.title')}</span>
|
<span className="block gradient-text">{t('home.title')}</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="text-base sm:text-lg md:text-xl lg:text-2xl text-gray-200 mb-3 md:mb-4 max-w-2xl mx-auto leading-relaxed font-bold uppercase tracking-wide px-4">
|
<p className="text-sm xs:text-base sm:text-lg md:text-xl lg:text-2xl text-gray-200 mb-2 md:mb-4 max-w-2xl mx-auto leading-relaxed font-bold uppercase tracking-wide px-2 xs:px-4">
|
||||||
{t('home.subtitle')}
|
{t('home.subtitle')}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm md:text-base text-gray-400 max-w-xl mx-auto px-4 mb-8">
|
<p className="text-xs xs:text-sm md:text-base text-gray-400 max-w-xl mx-auto px-2 xs:px-4 mb-6 xs:mb-8">
|
||||||
{t('home.description')}
|
{t('home.description')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="space-y-3 mb-8">
|
<div className="flex flex-wrap items-center justify-center gap-2 mb-6 xs:mb-8 px-2">
|
||||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-yellow-500/20 border border-yellow-500/50">
|
<div className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-yellow-500/20 border border-yellow-500/50">
|
||||||
<span className="text-yellow-400 font-bold text-sm uppercase">
|
<span className="text-yellow-400 font-bold text-xs uppercase">
|
||||||
⚠️ {t('home.betaWarning')}
|
⚠️ {t('home.betaWarning')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-blue-500/20 border border-blue-500/50 ml-2">
|
<div className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-blue-500/20 border border-blue-500/50">
|
||||||
<span className="text-blue-400 font-bold text-sm uppercase">
|
<span className="text-blue-400 font-bold text-xs uppercase">
|
||||||
📱 {t('home.pwaHint')}
|
📱 {t('home.pwaHint')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 sm:gap-4 mb-6 px-4">
|
<div className="flex flex-col xs:flex-row items-stretch xs:items-center justify-center gap-2 xs:gap-3 sm:gap-4 mb-6 px-2 xs:px-4">
|
||||||
<Link to="/map" className="w-full sm:w-auto">
|
<Link to="/map" className="w-full xs:w-auto">
|
||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
className="w-full sm:w-auto gap-2 bg-[#ff0040] hover:bg-[#ff0040]/90 text-white border-2 border-[#ff0040] shadow-[0_0_20px_rgba(255,0,64,0.5)] font-bold uppercase px-6 sm:px-8 py-4 sm:py-6 text-sm sm:text-base"
|
className="w-full xs:w-auto gap-2 bg-[#ff0040] hover:bg-[#ff0040]/90 text-white border-2 border-[#ff0040] shadow-[0_0_20px_rgba(255,0,64,0.5)] font-bold uppercase px-5 sm:px-8 py-3 xs:py-4 sm:py-6 text-sm sm:text-base h-auto"
|
||||||
>
|
>
|
||||||
<MapPin className="h-4 w-4 sm:h-5 sm:w-5" />
|
<MapPin className="h-4 w-4 sm:h-5 sm:w-5 flex-shrink-0" />
|
||||||
{t('home.exploreMap')}
|
{t('home.exploreMap')}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
{user && (
|
{user && (
|
||||||
<Link to="/create-spot" className="w-full sm:w-auto">
|
<Link to="/create-spot" className="w-full xs:w-auto">
|
||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full sm:w-auto gap-2 border-2 border-[#ccff00] bg-transparent hover:bg-[#ccff00]/10 text-[#ccff00] shadow-[0_0_15px_rgba(204,255,0,0.3)] font-bold uppercase px-6 sm:px-8 py-4 sm:py-6 text-sm sm:text-base"
|
className="w-full xs:w-auto gap-2 border-2 border-[#ccff00] bg-transparent hover:bg-[#ccff00]/10 text-[#ccff00] shadow-[0_0_15px_rgba(204,255,0,0.3)] font-bold uppercase px-5 sm:px-8 py-3 xs:py-4 sm:py-6 text-sm sm:text-base h-auto"
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4 sm:h-5 sm:w-5" />
|
<Plus className="h-4 w-4 sm:h-5 sm:w-5 flex-shrink-0" />
|
||||||
{t('home.addSpot')}
|
{t('home.addSpot')}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,14 @@ export default {
|
||||||
'2xl': '1400px'
|
'2xl': '1400px'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
screens: {
|
||||||
|
'xs': '375px',
|
||||||
|
'sm': '640px',
|
||||||
|
'md': '768px',
|
||||||
|
'lg': '1024px',
|
||||||
|
'xl': '1280px',
|
||||||
|
'2xl': '1400px',
|
||||||
|
},
|
||||||
extend: {
|
extend: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['Outfit Variable', 'Outfit', 'system-ui', 'sans-serif'],
|
sans: ['Outfit Variable', 'Outfit', 'system-ui', 'sans-serif'],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue