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 (
{label}
);
}
// 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 (
{displayName.slice(0, 2).toUpperCase()}
{/* Status indicator */}
In
{/* Green online indicator dot */}
{/* User info header */}
{displayName.slice(0, 2).toUpperCase()}
{/* Set Status - highlighted */}
Set Status
🛹
Add Spot
My Profile
My Data
My Stash
Wallet
Log Out
);
}
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 bar */}
{tabs.map((item) => {
const active = isActive(item.href);
const isWallet = item.href === '/wallet';
return (
{/* Active indicator dot */}
{active && (
)}
{isWallet ? (
{cashuBalance > 0 && (
{formatSatsCompact(cashuBalance)}
)}
) : (
)}
{t(item.labelKey)}
);
})}
{/* Profile / Login tab */}
{isActive('/settings') && (
)}
{isLoggedIn && userPubkey ? (
{(userName || '??').slice(0, 2).toUpperCase()}
) : (
)}
{isLoggedIn ? 'Me' : 'Log In'}
);
}
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 (
{/* Desktop Header */}
{/* Mobile Header */}
SKATESPOTS
{/* User menu when logged in / Login button when not */}
{user ? (
removeLogin(currentAccount?.id || '')}
onSetStatus={() => setShowStatusDialog(true)}
/>
) : (
)}
{/* Wallet Card - Mobile Menu */}
{user && (
setMobileMenuOpen(false)}
className="block"
>
{t('wallet.cashu.title')}
Ecash
{hasCashu && (
{cashuBalance.toLocaleString()}
sats
)}
)}
{navItemKeys.map((item) => (
setMobileMenuOpen(false)}
/>
))}
{user && (
setMobileMenuOpen(false)}
/>
)}
{user && (
setMobileMenuOpen(false)}
/>
)}
{user && (
setMobileMenuOpen(false)}
/>
)}
setMobileMenuOpen(false)}
/>
setMobileMenuOpen(false)}
/>
setMobileMenuOpen(false)}
/>
setMobileMenuOpen(false)}
/>
setMobileMenuOpen(false)}
/>
setMobileMenuOpen(false)}
/>
{/* Language Toggle in Mobile Menu */}
{/* Login/Logout Section */}
{user ? (
<>
{/* User Profile Card */}
{displayName.slice(0, 2).toUpperCase()}
{/* Logout Button - Big and Clear */}
{
removeLogin(currentAccount?.id || '');
setMobileMenuOpen(false);
}}
>
Log Out
>
) : (
<>
{/* Not Logged In - Clear CTA */}
Not Logged In
Log in to add spots, zap skaters, and save your favorites
>
)}
{/* Backup Reminder */}
{/* Main Content — extra bottom padding on mobile so the fixed bottom nav never overlaps */}
{children}
{/* Footer - Desktop */}
Made with ❤️ for the skateboarding community
License
Supporters
Not sponsored by
OpenSats
{/* No mobile footer - bottom nav provides navigation, keeps UI clean */}
{/* Mobile Bottom Navigation */}
{/* Sasquatch Guide (Desktop only) */}
{/* Set Status Dialog */}
);
}