spb/satelliteindex
Public
TypeScript 66.5%
Python 30.9%
JavaScript 1.4%
CSS 0.7%
1'use client';2import { Activity, BarChart3, Globe2, MoreHorizontal, Search } from 'lucide-react';3import Link from 'next/link';4import { usePathname } from 'next/navigation';5import { useEffect, useState } from 'react';6import { cn } from '@/lib/cn';7import { moreNav, primaryNav, routes } from '@/lib/site';8import { useOpenSearch } from './search-context';910/**11 * Native-feeling bottom tab bar (< md): Explore · Search · Events · Stats · More. Safe-area aware; the body reserves12 * `pb-[calc(58px+env(safe-area-inset-bottom))]` so it never covers the footer. Hidden on /explore (full-screen globe has its own controls).13 */14export function MobileTabBar() {15 const pathname = usePathname();16 const openSearch = useOpenSearch();17 const [more, setMore] = useState(false);18 useEffect(() => setMore(false), [pathname]);19 const tabs = [20 { href: routes.explore(), label: 'Explore', Icon: Globe2, match: (p: string) => p === '/' || p.startsWith('/explore') || p.startsWith('/satellite') },21 { href: routes.events(), label: 'Events', Icon: Activity, match: (p: string) => p.startsWith('/events') },22 { href: routes.stats(), label: 'Stats', Icon: BarChart3, match: (p: string) => p.startsWith('/stats') || p.startsWith('/rankings') },23 ];24 return (25 <>26 {more && (27 <div className="fixed inset-0 z-[45] bg-black/50 md:hidden" onClick={() => setMore(false)}>28 <div className="panel absolute inset-x-2 bottom-[calc(var(--tabbar-h)+env(safe-area-inset-bottom,0px)+8px)] max-h-[70vh] overflow-y-auto p-3" onClick={(e) => e.stopPropagation()}>29 <p className="eyebrow px-2 pb-2">Browse</p>30 <ul className="grid grid-cols-2 gap-1">31 {[...primaryNav, ...moreNav].map((n) => (32 <li key={n.href}>33 <Link href={n.href} className="block rounded-md px-3 py-3 text-[15px] text-ink-2 hover:bg-plane-2 hover:text-ink">{n.label}</Link>34 </li>35 ))}36 </ul>37 </div>38 </div>39 )}40 <nav aria-label="Primary (mobile)" className="safe-bottom fixed inset-x-0 bottom-0 z-[46] border-t border-rule bg-plane/95 backdrop-blur-md md:hidden">41 <ul className="grid h-[58px] grid-cols-5">42 {tabs.slice(0, 1).map(({ href, label, Icon, match }) => (43 <TabLink key={href} href={href} label={label} Icon={Icon} active={match(pathname)} />44 ))}45 <li className="min-w-0">46 <button type="button" onClick={openSearch} className="flex h-full w-full flex-col items-center justify-center gap-0.5 text-2xs text-ink-2">47 <Search size={21} aria-hidden strokeWidth={1.75} />48 <span className="truncate">Search</span>49 </button>50 </li>51 {tabs.slice(1).map(({ href, label, Icon, match }) => (52 <TabLink key={href} href={href} label={label} Icon={Icon} active={match(pathname)} />53 ))}54 <li className="min-w-0">55 <button type="button" onClick={() => setMore((m) => !m)} aria-expanded={more} className={cn('flex h-full w-full flex-col items-center justify-center gap-0.5 text-2xs', more ? 'text-accent' : 'text-ink-2')}>56 <MoreHorizontal size={21} aria-hidden strokeWidth={1.75} />57 <span className="truncate">More</span>58 </button>59 </li>60 </ul>61 </nav>62 </>63 );64}6566function TabLink({ href, label, Icon, active }: { href: string; label: string; Icon: typeof Globe2; active: boolean }) {67 return (68 <li className="min-w-0">69 <Link href={href} aria-current={active ? 'page' : undefined} className={cn('flex h-full flex-col items-center justify-center gap-0.5 text-2xs', active ? 'text-accent' : 'text-ink-2')}>70 <Icon size={21} aria-hidden strokeWidth={active ? 2.25 : 1.75} />71 <span className="truncate">{label}</span>72 </Link>73 </li>74 );75}76