- Install @fontsource/bebas-neue and @fontsource/barlow-condensed - Add .font-display (Bebas Neue) and .font-condensed (Barlow Condensed) CSS utilities - Hero headline: split into two lines SKATE / SPOTS with clamp() fluid sizing, Bebas Neue replaces Impact throughout - Subtitle: Barlow Condensed 700 with wide letter-spacing - Description: Barlow Condensed italic - All section headings (Featured Spots, Events, The Lab, CTA, New Here?): Bebas Neue - Section subheadings: Barlow Condensed with tracked uppercase - Desktop + mobile header logo: Bebas Neue - Footer logo: Bebas Neue - Tagline badge: Barlow Condensed 600 Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
820 lines
33 KiB
TypeScript
820 lines
33 KiB
TypeScript
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,
|
||
Gift,
|
||
LogOut,
|
||
User,
|
||
LogIn,
|
||
Plus,
|
||
CheckCircle2,
|
||
Database,
|
||
Package,
|
||
Zap,
|
||
Heart,
|
||
CreditCard,
|
||
} 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';
|
||
|
||
interface LayoutProps {
|
||
children: React.ReactNode;
|
||
}
|
||
|
||
// Nav item keys that map to translation keys
|
||
const navItemKeys = [
|
||
{ href: '/', labelKey: 'nav.home', icon: Home },
|
||
{ href: '/map', 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</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 */}
|
||
<Link
|
||
to="/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 }: 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) => {
|
||
if (path === '/') return location.pathname === '/';
|
||
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">
|
||
<div className="relative">
|
||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-red-500 to-orange-500 flex items-center justify-center shadow-lg">
|
||
<MapPin className="h-5 w-5 text-white" />
|
||
</div>
|
||
</div>
|
||
<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('/bounties') || 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="/bounties" className="flex items-center gap-2 cursor-pointer">
|
||
<Gift className="h-4 w-4 text-orange-400" />
|
||
<span className="text-orange-400">Map & Earn</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>
|
||
</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">
|
||
<div className="w-9 h-9 rounded-lg bg-gradient-to-br from-red-500 to-orange-500 flex items-center justify-center flex-shrink-0 shadow-lg">
|
||
<MapPin className="h-4 w-4 text-white" />
|
||
</div>
|
||
<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">
|
||
<div className="flex flex-col gap-6 mt-6">
|
||
{/* 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="/bounties"
|
||
label="Map & Earn"
|
||
icon={Gift}
|
||
isActive={isActive('/bounties')}
|
||
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="/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 */}
|
||
<main className="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="/license"
|
||
className="hover:text-[#ccff00] transition-colors font-medium"
|
||
>
|
||
License
|
||
</Link>
|
||
<Link
|
||
to="/supporters"
|
||
className="hover:text-[#ccff00] transition-colors font-medium"
|
||
>
|
||
Supporters
|
||
</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 (Desktop only) */}
|
||
<div className="hidden md:block">
|
||
<SasquatchGuide
|
||
message={currentMessage}
|
||
onDismiss={dismissCurrentMessage}
|
||
/>
|
||
</div>
|
||
|
||
{/* Set Status Dialog */}
|
||
<SetStatusDialog
|
||
open={showStatusDialog}
|
||
onOpenChange={setShowStatusDialog}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|