SkateSpots/src/components/Layout.tsx
shakespeare.diy a1cd936bc4 UX audit round 2: Near Me fix, map height, touch targets, iOS dirs, a11y, mobile layout
CRITICAL:
- Near Me button: completely rewritten — was silently discarding ALL errors. Now shows
  toast for denied, timeout, unavailable with actionable copy. Button shows red LocateOff
  icon when denied so user sees the state at a glance. Raised to 44×44px (Apple minimum).
- Map height on mobile: switched to CSS variable system (--header-mobile, --bottom-nav)
  for pixel-accurate height. Added Layout noPadding prop so the map page gets full bleed
  without the 80px bottom-padding fighting the map container calc.
- Layer toggle pills: raised from 28×28 to 40×40px — was below touch target minimum
- Right-side control column: now hides when spot bottom sheet is open (was always visible)

iOS/Directions:
- SpotDetailPage directions button: now detects Apple platform and opens Apple Maps on
  iOS/macOS, Google Maps everywhere else (was always Google Maps)
- Added aria-label to directions button

Accessibility:
- LoginDialog: was missing DialogTitle, causing console errors on every login open
- CameraCapture: fallback dialog was missing DialogTitle (sr-only)
- CommandDialog: added sr-only DialogTitle

Layout:
- SpotDetailPage: reduced top padding mobile pt-4 (was py-8 = 32px wasted space)
- Directions popover: centered in viewport (top-1/2 left-1/2) with zoom-in animation,
  never clips behind Sheet stacking context

CSS:
- Added --header-mobile and --bottom-nav CSS variables for consistent calculations
- Added .page-fullbleed helper

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-23 21:56:11 +07:00

835 lines
34 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

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

import { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from '@/components/ui/dropdown-menu';
import { LoginArea } from '@/components/auth/LoginArea';
import { SasquatchGuide } from '@/components/SasquatchGuide';
import { useSasquatchGuide } from '@/hooks/useSasquatchGuide';
import { BackupReminder } from '@/components/BackupReminder';
import { LanguageToggle } from '@/components/LanguageToggle';
import { useCashu } from '@/hooks/wallet/useCashu';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useTranslation } from '@/contexts/LanguageContext';
import { useLoggedInAccounts } from '@/hooks/useLoggedInAccounts';
import {
MapPin,
Calendar,
Menu,
Home,
Wallet,
Settings,
GraduationCap,
BarChart3,
MoreHorizontal,
Info,
Bookmark,
MessageCircle,
LogOut,
User,
LogIn,
Plus,
CheckCircle2,
Database,
Package,
Zap,
Heart,
CreditCard,
Radio,
} from 'lucide-react';
import { OfflineIndicator } from '@/components/OfflineIndicator';
import { UserStatusIndicator } from '@/components/UserStatusIndicator';
import { SetStatusDialog } from '@/components/SetStatusDialog';
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
import { useAuthor } from '@/hooks/useAuthor';
import { genUserName } from '@/lib/genUserName';
import { NewsletterOverlay } from '@/components/NewsletterOverlay';
interface LayoutProps {
children: React.ReactNode;
/** If true, removes the bottom padding that prevents bottom nav overlap.
* Use for full-bleed pages like the map that manage their own height. */
noPadding?: boolean;
}
// Nav item keys that map to translation keys
// HOME removed — the map IS the homepage now
const navItemKeys = [
{ href: '/', labelKey: 'nav.map', icon: MapPin },
{ href: '/lab', labelKey: 'nav.lab', icon: GraduationCap },
{ href: '/events', labelKey: 'nav.events', icon: Calendar },
];
// Format sats for display (e.g., 1234 -> "1.2k", 12345 -> "12k")
function formatSatsCompact(sats: number): string {
if (sats >= 1000000) {
return `${(sats / 1000000).toFixed(1)}M`;
}
if (sats >= 1000) {
return `${(sats / 1000).toFixed(sats >= 10000 ? 0 : 1)}k`;
}
return sats.toString();
}
function NavLink({
href,
label,
icon: Icon,
isActive,
onClick,
}: {
href: string;
label: string;
icon: React.ComponentType<{ className?: string }>;
isActive: boolean;
onClick?: () => void;
}) {
return (
<Link
to={href}
onClick={onClick}
className={cn(
'flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-200',
isActive
? 'bg-primary text-primary-foreground shadow-lg shadow-primary/25'
: 'hover:bg-muted text-muted-foreground hover:text-foreground'
)}
>
<Icon className="h-5 w-5" />
<span className="font-medium">{label}</span>
</Link>
);
}
// Mobile header user menu - shows when logged in with dropdown for actions
function MobileHeaderUserMenu({
pubkey,
onLogout,
onSetStatus,
}: {
pubkey: string;
onLogout: () => void;
onSetStatus: () => void;
}) {
const author = useAuthor(pubkey);
const metadata = author.data?.metadata;
const displayName = metadata?.name ?? genUserName(pubkey);
const avatar = metadata?.picture;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="touch-manipulation relative flex items-center gap-2 px-2 py-1.5 rounded-full bg-green-500/10 border border-green-500/30 active:scale-95 transition-transform">
<Avatar className="h-7 w-7 border-2 border-green-500/50">
<AvatarImage src={avatar} alt={displayName} />
<AvatarFallback className="bg-green-500/20 text-green-400 text-[10px] font-bold">
{displayName.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
{/* Status indicator */}
<UserStatusIndicator pubkey={pubkey} size="sm" />
<span className="text-green-400 text-xs font-bold uppercase tracking-wide hidden xs:inline">
<CheckCircle2 className="h-3 w-3 inline mr-1" />
In
</span>
{/* Green online indicator dot */}
<span className="absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 bg-green-500 border-2 border-black rounded-full xs:hidden" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56 p-2">
{/* User info header */}
<div className="px-2 py-3 mb-2 rounded-lg bg-green-500/10 border border-green-500/20">
<div className="flex items-center gap-3">
<div className="relative">
<Avatar className="h-10 w-10 border-2 border-green-500/50">
<AvatarImage src={avatar} alt={displayName} />
<AvatarFallback className="bg-green-500/20 text-green-400 text-sm font-bold">
{displayName.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<UserStatusIndicator pubkey={pubkey} size="sm" className="absolute -bottom-1 -right-1" />
</div>
<div className="flex-1 min-w-0">
<p className="font-bold text-sm truncate">{displayName}</p>
<p className="text-xs text-green-500 flex items-center gap-1">
<CheckCircle2 className="h-3 w-3" />
Logged In
</p>
</div>
</div>
</div>
{/* Set Status - highlighted */}
<DropdownMenuItem
onClick={onSetStatus}
className="flex items-center gap-2 cursor-pointer mb-1 bg-green-500/10 border border-green-500/30 rounded-lg"
>
<span className="relative flex h-2.5 w-2.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-green-500" />
</span>
<span className="text-green-400 font-bold">Set Status</span>
<span className="ml-auto text-xs text-green-500">🛹</span>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/create-spot" className="flex items-center gap-2 cursor-pointer">
<Plus className="h-4 w-4 text-[#ccff00]" />
<span className="text-[#ccff00] font-medium">Add Spot</span>
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/settings" className="flex items-center gap-2 cursor-pointer">
<User className="h-4 w-4" />
My Profile
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/settings?tab=mydata" className="flex items-center gap-2 cursor-pointer">
<Database className="h-4 w-4 text-[#ccff00]" />
<span className="text-[#ccff00]">My Data</span>
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/stash" className="flex items-center gap-2 cursor-pointer">
<Package className="h-4 w-4 text-purple-400" />
<span className="text-purple-400">My Stash</span>
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/wallet" className="flex items-center gap-2 cursor-pointer">
<Wallet className="h-4 w-4 text-orange-400" />
<span className="text-orange-400">Wallet &amp; Stash</span>
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={onLogout}
className="flex items-center gap-2 cursor-pointer text-red-400"
>
<LogOut className="h-4 w-4" />
Log Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
function MobileNav({
isActive,
cashuBalance,
isLoggedIn,
userPubkey,
userAvatar,
userName,
t,
}: {
isActive: (path: string) => boolean;
cashuBalance: number;
isLoggedIn: boolean;
userPubkey?: string;
userAvatar?: string;
userName?: 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 (
<nav className="fixed bottom-0 left-0 right-0 z-50 md:hidden">
{/* Nav bar */}
<div
className="glass border-t border-white/10"
style={{ paddingBottom: 'env(safe-area-inset-bottom, 0px)', paddingLeft: 'env(safe-area-inset-left, 0px)', paddingRight: 'env(safe-area-inset-right, 0px)' }}
>
<div className="flex items-stretch justify-around px-1 pt-1 pb-1">
{tabs.map((item) => {
const active = isActive(item.href);
const isWallet = item.href === '/wallet';
return (
<Link
key={item.href}
to={item.href}
className={cn(
'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',
active
? isWallet ? 'text-orange-400' : 'text-[#ff0040]'
: 'text-gray-500'
)}
>
{/* Active indicator dot */}
{active && (
<span className={cn(
'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 — goes straight to profile section when logged in */}
<Link
to={isLoggedIn ? "/settings?section=profile" : "/settings"}
className={cn(
'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') ? 'text-[#ff0040]' : 'text-gray-500'
)}
>
{isActive('/settings') && (
<span className="absolute top-0 left-1/2 -translate-x-1/2 w-1 h-1 rounded-full bg-[#ff0040]" />
)}
{isLoggedIn && userPubkey ? (
<div className="relative">
<Avatar className={cn(
'h-[22px] w-[22px] border-2 transition-transform',
isActive('/settings') ? 'border-[#ff0040] scale-110' : 'border-gray-600'
)}>
<AvatarImage src={userAvatar} alt={userName} />
<AvatarFallback className="bg-[#ff0040]/20 text-[#ff0040] text-[8px] font-bold">
{(userName || '??').slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<span className="absolute -bottom-0.5 -right-0.5 w-2 h-2 bg-green-500 border border-black rounded-full" />
</div>
) : (
<User className="h-[22px] w-[22px]" />
)}
<span className="text-[9px] xs:text-[10px] font-semibold leading-tight tracking-wide uppercase">
{isLoggedIn ? 'Me' : 'Log In'}
</span>
</Link>
</div>
</div>
</nav>
);
}
export function Layout({ children, noPadding }: LayoutProps) {
const location = useLocation();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [showStatusDialog, setShowStatusDialog] = useState(false);
const { currentMessage, dismissCurrentMessage } = useSasquatchGuide();
const { user } = useCurrentUser();
const { totalBalance: cashuBalance, isInitialized: hasCashu } = useCashu();
const { t } = useTranslation();
const { currentUser: currentAccount, removeLogin } = useLoggedInAccounts();
const author = useAuthor(user?.pubkey || '');
const displayName = author.data?.metadata?.name ?? genUserName(user?.pubkey);
const userAvatar = author.data?.metadata?.picture;
const isActive = (path: string) => {
// Both / and /map are the "spots" page
if (path === '/') return location.pathname === '/' || location.pathname === '/map';
return location.pathname.startsWith(path);
};
return (
<div className="min-h-screen bg-black text-white">
{/* Desktop Header */}
<header className="hidden md:flex sticky top-0 z-50 border-b border-red-500/30 bg-black/95 backdrop-blur">
<div className="container flex items-center justify-between h-16 px-4">
<Link to="/" className="flex items-center gap-3 group">
<img
src="/images/icon-1024.png"
alt="SkateSpots"
className="w-10 h-10 rounded-xl shadow-lg shadow-red-500/30 group-hover:shadow-red-500/50 transition-shadow"
/>
<span className="font-display text-2xl neon-red tracking-wide" style={{
textShadow: '0 0 10px #ff0040, 0 0 20px #ff0040'
}}>SKATESPOTS</span>
</Link>
<nav className="flex items-center gap-1">
{navItemKeys.map((item) => (
<Link
key={item.href}
to={item.href}
className={cn(
'flex items-center gap-2 px-4 py-2 rounded-lg transition-all duration-200 text-sm font-bold uppercase tracking-wide',
isActive(item.href)
? 'text-red-500'
: 'text-gray-400 hover:text-red-400'
)}
>
<item.icon className="h-4 w-4" />
{t(item.labelKey)}
</Link>
))}
{/* More Menu - Desktop */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className={cn(
'flex items-center gap-2 px-4 py-2 rounded-lg transition-all duration-200 text-sm font-bold uppercase tracking-wide h-auto',
(isActive('/stats') || isActive('/about'))
? 'text-red-500'
: 'text-gray-400 hover:text-red-400'
)}
>
<MoreHorizontal className="h-4 w-4" />
{t('nav.more')}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
{user && (
<DropdownMenuItem asChild>
<Link to="/collections" className="flex items-center gap-2 cursor-pointer">
<Bookmark className="h-4 w-4" />
My Collections
</Link>
</DropdownMenuItem>
)}
{user && (
<DropdownMenuItem asChild>
<Link to="/stash" className="flex items-center gap-2 cursor-pointer">
<Package className="h-4 w-4 text-purple-400" />
<span className="text-purple-400">My Stash</span>
</Link>
</DropdownMenuItem>
)}
<DropdownMenuItem asChild>
<Link to="/stats" className="flex items-center gap-2 cursor-pointer">
<BarChart3 className="h-4 w-4" />
{t('nav.stats')}
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link to="/about" className="flex items-center gap-2 cursor-pointer">
<Info className="h-4 w-4" />
{t('nav.about')}
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/card" className="flex items-center gap-2 cursor-pointer">
<CreditCard className="h-4 w-4 text-[#ccff00]" />
<span className="text-[#ccff00]">Rider Card</span>
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/supporters" className="flex items-center gap-2 cursor-pointer">
<Heart className="h-4 w-4 text-[#ff0040]" />
<span className="text-[#ff0040]">Supporters ⚡</span>
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link to="/newsletter" className="flex items-center gap-2 cursor-pointer">
<Radio className="h-4 w-4 text-[#ccff00]" />
<span className="text-[#ccff00]">Newsletter</span>
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</nav>
<div className="flex items-center gap-3">
{/* Messages Quick Access - Desktop */}
{user && (
<Link to="/messages">
<Button
variant="ghost"
size="icon"
className={cn(
isActive('/messages')
? "text-[#ff0040] bg-[#ff0040]/10"
: "text-gray-400 hover:text-[#ff0040] hover:bg-[#ff0040]/10"
)}
title="Messages"
>
<MessageCircle className="h-5 w-5" />
</Button>
</Link>
)}
{/* Collections Quick Access - Desktop */}
{user && (
<Link to="/collections">
<Button
variant="ghost"
size="icon"
className={cn(
isActive('/collections')
? "text-[#ccff00] bg-[#ccff00]/10"
: "text-gray-400 hover:text-[#ccff00] hover:bg-[#ccff00]/10"
)}
title="My Collections"
>
<Bookmark className="h-5 w-5" />
</Button>
</Link>
)}
{/* Wallet Quick Access - Desktop */}
{user && (
<Link to="/wallet">
<Button
variant="ghost"
className={cn(
"gap-2 px-3",
isActive('/wallet')
? "text-orange-500 bg-orange-500/10"
: "text-gray-400 hover:text-orange-400 hover:bg-orange-500/10"
)}
>
<Wallet className="h-5 w-5" />
{hasCashu && cashuBalance > 0 && (
<span className="text-sm font-bold">
{formatSatsCompact(cashuBalance)}
</span>
)}
</Button>
</Link>
)}
{/* Status Quick Access - Desktop */}
{user && (
<Button
variant="outline"
className="gap-2 px-3 border-green-500/50 text-green-400 hover:text-green-300 hover:bg-green-500/10 hover:border-green-500"
onClick={() => setShowStatusDialog(true)}
title="Set Your Status"
>
<span className="relative flex h-2.5 w-2.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-green-500" />
</span>
<span className="text-sm font-bold">Status</span>
</Button>
)}
{/* Offline Indicator */}
<OfflineIndicator />
{/* Language Toggle */}
<LanguageToggle compact className="text-gray-400 hover:text-white" />
<Link to="/settings">
<Button variant="ghost" size="icon">
<Settings className="h-5 w-5" />
</Button>
</Link>
<LoginArea className="max-w-60" />
</div>
</div>
</header>
{/* Mobile Header */}
<header className="md:hidden sticky top-0 z-50 border-b border-red-500/30 bg-black/95 backdrop-blur safe-area-top">
<div className="flex items-center justify-between h-14 px-3 gap-3">
<Link to="/" className="flex items-center gap-2 touch-manipulation active:opacity-80 flex-1 min-w-0">
<img
src="/images/icon-1024.png"
alt="SkateSpots"
className="w-9 h-9 rounded-lg flex-shrink-0 shadow-lg shadow-red-500/30"
/>
<span className="font-display text-xl neon-red tracking-wide" style={{
textShadow: '0 0 8px #ff0040'
}}>SKATESPOTS</span>
</Link>
<div className="flex items-center gap-1 flex-shrink-0">
{/* User menu when logged in / Login button when not */}
{user ? (
<MobileHeaderUserMenu
pubkey={user.pubkey}
onLogout={() => removeLogin(currentAccount?.id || '')}
onSetStatus={() => setShowStatusDialog(true)}
/>
) : (
<LoginArea compact className="flex flex-row gap-1" />
)}
<Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="h-10 w-10 touch-manipulation active:scale-95 transition-transform">
<Menu className="h-5 w-5" />
</Button>
</SheetTrigger>
<SheetContent side="right" className="w-80 overflow-y-auto">
<div className="flex flex-col gap-6 mt-6 pb-8">
{/* Wallet Card - Mobile Menu */}
{user && (
<Link
to="/wallet"
onClick={() => setMobileMenuOpen(false)}
className="block"
>
<div className={cn(
"p-4 rounded-xl border-2 transition-all",
isActive('/wallet')
? "bg-orange-500/20 border-orange-500/50"
: "bg-orange-500/10 border-orange-500/30 hover:bg-orange-500/20"
)}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-orange-500/20 flex items-center justify-center">
<Wallet className="h-5 w-5 text-orange-500" />
</div>
<div>
<p className="font-bold text-sm">{t('wallet.cashu.title')}</p>
<p className="text-xs text-muted-foreground">Ecash</p>
</div>
</div>
{hasCashu && (
<div className="text-right">
<p className="font-bold text-orange-500">
{cashuBalance.toLocaleString()}
</p>
<p className="text-xs text-muted-foreground">sats</p>
</div>
)}
</div>
</div>
</Link>
)}
<div className="flex flex-col gap-2">
{navItemKeys.map((item) => (
<NavLink
key={item.href}
href={item.href}
label={t(item.labelKey)}
icon={item.icon}
isActive={isActive(item.href)}
onClick={() => setMobileMenuOpen(false)}
/>
))}
{user && (
<NavLink
href="/messages"
label="Messages"
icon={MessageCircle}
isActive={isActive('/messages')}
onClick={() => setMobileMenuOpen(false)}
/>
)}
{user && (
<NavLink
href="/collections"
label="My Collections"
icon={Bookmark}
isActive={isActive('/collections')}
onClick={() => setMobileMenuOpen(false)}
/>
)}
{user && (
<NavLink
href="/stash"
label="My Stash"
icon={Package}
isActive={isActive('/stash')}
onClick={() => setMobileMenuOpen(false)}
/>
)}
<NavLink
href="/stats"
label={t('nav.stats')}
icon={BarChart3}
isActive={isActive('/stats')}
onClick={() => setMobileMenuOpen(false)}
/>
<NavLink
href="/about"
label={t('nav.about')}
icon={Info}
isActive={isActive('/about')}
onClick={() => setMobileMenuOpen(false)}
/>
<NavLink
href="/card"
label="Rider Card"
icon={CreditCard}
isActive={isActive('/card')}
onClick={() => setMobileMenuOpen(false)}
/>
<NavLink
href="/supporters"
label="Supporters ⚡"
icon={Heart}
isActive={isActive('/supporters')}
onClick={() => setMobileMenuOpen(false)}
/>
<NavLink
href="/newsletter"
label="Newsletter"
icon={Radio}
isActive={isActive('/newsletter')}
onClick={() => setMobileMenuOpen(false)}
/>
<NavLink
href="/settings"
label={t('nav.settings')}
icon={Settings}
isActive={isActive('/settings')}
onClick={() => setMobileMenuOpen(false)}
/>
</div>
{/* Language Toggle in Mobile Menu */}
<div className="py-3 border-t">
<LanguageToggle className="w-full justify-start" variant="ghost" />
</div>
{/* Login/Logout Section */}
<div className="pt-4 border-t space-y-3">
{user ? (
<>
{/* User Profile Card */}
<div className="p-4 rounded-xl bg-gradient-to-br from-green-500/10 to-green-500/5 border border-green-500/20">
<div className="flex items-center gap-3">
<Avatar className="h-12 w-12 border-2 border-green-500/50">
<AvatarImage src={author.data?.metadata?.picture} alt={displayName} />
<AvatarFallback className="bg-[#ff0040]/20 text-[#ff0040] text-sm font-bold">
{displayName.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<p className="font-bold text-base truncate">{displayName}</p>
<p className="text-sm text-green-500 flex items-center gap-1.5">
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
Logged In
</p>
</div>
</div>
</div>
{/* Logout Button - Big and Clear */}
<Button
variant="outline"
size="lg"
className="w-full justify-center gap-3 text-red-400 border-red-500/50 hover:bg-red-500/20 hover:text-red-300 hover:border-red-500 h-12 text-base font-bold"
onClick={() => {
removeLogin(currentAccount?.id || '');
setMobileMenuOpen(false);
}}
>
<LogOut className="h-5 w-5" />
Log Out
</Button>
</>
) : (
<>
{/* Not Logged In - Clear CTA */}
<div className="p-4 rounded-xl bg-gradient-to-br from-[#ff0040]/10 to-[#ff0040]/5 border border-[#ff0040]/20 text-center">
<User className="h-10 w-10 mx-auto mb-2 text-[#ff0040]/50" />
<p className="font-bold text-base mb-1">Not Logged In</p>
<p className="text-sm text-muted-foreground mb-4">
Log in to add spots, zap skaters, and save your favorites
</p>
<LoginArea className="w-full justify-center" />
</div>
</>
)}
</div>
</div>
</SheetContent>
</Sheet>
</div>
</div>
</header>
{/* Backup Reminder */}
<BackupReminder />
{/* Main Content — extra bottom padding on mobile so the fixed bottom nav never overlaps */}
{/* noPadding: full-bleed pages (map) manage their own height */}
<main className={noPadding ? undefined : "pb-[calc(5rem+env(safe-area-inset-bottom,0px))] md:pb-6"}>
{children}
</main>
{/* Footer - Desktop */}
<footer className="hidden md:block border-t border-gray-800 bg-black/95 py-6">
<div className="container px-4">
<div className="flex flex-col md:flex-row items-center justify-between gap-4 text-sm text-gray-400">
<span>Made with <span className="text-red-500">❤️</span> for the skateboarding community</span>
<div className="flex items-center gap-4 text-xs text-gray-500">
<Link
to="/newsletter"
className="hover:text-[#ccff00] transition-colors font-medium"
>
Newsletter
</Link>
<Link
to="/license"
className="hover:text-[#ccff00] transition-colors font-medium"
>
License
</Link>
<Link
to="/supporters"
className="hover:text-[#ccff00] transition-colors font-medium"
>
Collaborators &amp; Friends
</Link>
<span className="flex items-center gap-1.5">
<span>Not sponsored by</span>
<span className="inline-flex items-center gap-1.5">
<img
src="https://opensats.org/favicon.ico"
alt="OpenSats"
className="h-4 w-4"
/>
<span className="font-medium text-gray-400">OpenSats</span>
</span>
</span>
</div>
</div>
</div>
</footer>
{/* No mobile footer - bottom nav provides navigation, keeps UI clean */}
{/* Mobile Bottom Navigation */}
<MobileNav
isActive={isActive}
cashuBalance={cashuBalance}
isLoggedIn={!!user}
userPubkey={user?.pubkey}
userAvatar={userAvatar}
userName={displayName}
t={t}
/>
{/* Sasquatch Guide (all screens) */}
<SasquatchGuide
message={currentMessage}
onDismiss={dismissCurrentMessage}
/>
{/* Set Status Dialog */}
<SetStatusDialog
open={showStatusDialog}
onOpenChange={setShowStatusDialog}
/>
{/* Newsletter: floating pill + Monday admin reminder */}
<NewsletterOverlay />
</div>
);
}