TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1'use client';23import Link from 'next/link';4import { useEffect, useState } from 'react';5import { usePathname } from 'next/navigation';6import { ChevronRight, Menu, X } from 'lucide-react';7import type { NavItem } from '@/config/nav';8import { LogoMark } from './logo';910export function MobileNav({ items, groups }: { items: NavItem[]; groups: Array<{ id: NavItem['group']; label: string }> }) {11 const [open, setOpen] = useState(false);12 const pathname = usePathname();13 const [prevPath, setPrevPath] = useState(pathname);14 if (pathname !== prevPath) {15 // close the drawer on navigation (state adjustment during render, no effect needed)16 setPrevPath(pathname);17 setOpen(false);18 }19 useEffect(() => {20 document.body.style.overflow = open ? 'hidden' : '';21 const onKey = (e: KeyboardEvent) => e.key === 'Escape' && setOpen(false);22 window.addEventListener('keydown', onKey);23 return () => {24 document.body.style.overflow = '';25 window.removeEventListener('keydown', onKey);26 };27 }, [open]);28 return (29 <div className="lg:hidden">30 <button type="button" aria-label="Open menu" aria-expanded={open} className="-ml-1 inline-flex h-10 w-10 items-center justify-center rounded-md text-fg hover:bg-inset" onClick={() => setOpen(true)}>31 <Menu className="h-5 w-5" />32 </button>33 {open ? (34 <div className="fixed inset-0 z-[65] flex" role="dialog" aria-modal="true" aria-label="Menu">35 <div className="absolute inset-0 bg-black/45 backdrop-blur-[2px]" onClick={() => setOpen(false)} />36 <div className="sheet-in relative flex h-[100dvh] w-[88vw] max-w-sm flex-col bg-elevated shadow-pop">37 <div className="flex h-[var(--ri-header-h)] items-center justify-between border-b border-border px-4" style={{ marginTop: 'env(safe-area-inset-top)' }}>38 <span className="flex items-center gap-2">39 <LogoMark className="h-6 w-6" />40 <span className="text-[15px] font-semibold tracking-tight">41 Rare<span className="text-muted">Index</span>42 </span>43 </span>44 <button type="button" aria-label="Close menu" className="inline-flex h-10 w-10 items-center justify-center rounded-md text-muted hover:bg-inset" onClick={() => setOpen(false)}>45 <X className="h-5 w-5" />46 </button>47 </div>48 <nav className="flex-1 overflow-y-auto overscroll-contain px-2 py-3 safe-bottom">49 {groups.map((g) => (50 <div key={g.id} className="mb-3">51 <p className="t-label px-2 pb-1">{g.label}</p>52 {items53 .filter((n) => n.group === g.id)54 .map((n) => {55 const active = pathname === n.href;56 return (57 <Link key={n.href} href={n.href} className={`flex min-h-[44px] items-center justify-between gap-3 rounded-md px-2 py-2 ${active ? 'bg-inset' : 'hover:bg-sunken'}`}>58 <span className="min-w-0">59 <span className={`block text-[15px] ${active ? 'font-medium text-fg' : 'text-fg'}`}>{n.label}</span>60 <span className="block truncate text-[11px] text-subtle">{n.description}</span>61 </span>62 <ChevronRight className="h-4 w-4 shrink-0 text-subtle" />63 </Link>64 );65 })}66 </div>67 ))}68 <div className="mt-2 grid grid-cols-2 gap-2 px-1">69 <Link href="/login" className="btn h-11">70 Sign in71 </Link>72 <Link href="/signup" className="btn-primary h-11">73 Create account74 </Link>75 </div>76 </nav>77 </div>78 </div>79 ) : null}80 </div>81 );82}83