fix: API JSON error responses + navbar with dropdowns

- Add backend/libs/go-http-errors for consistent JSON errors
- REST API: use writeMethodNotAllowed, writeNotFound, writeInternalError
- middleware, gateway, search: use httperrors.WriteJSON
- SPA: navbar with Explore/Tools/More dropdowns, initNavDropdowns()
- Next.js: Navbar component with dropdowns + mobile menu

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-02-16 03:09:53 -08:00
parent 53114e75fd
commit a36ab9d47c
16 changed files with 3979 additions and 3191 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import Link from 'next/link'
import Navbar from '@/components/common/Navbar'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
@@ -18,34 +18,7 @@ export default function RootLayout({
return (
<html lang="en">
<body className={inter.className}>
<nav className="bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700">
<div className="container mx-auto px-4">
<div className="flex items-center justify-between h-16">
<div className="flex items-center gap-8">
<Link href="/" className="text-xl font-bold text-primary-600">
<div className="flex flex-col">
<span>SolaceScanScout</span>
<span className="text-xs font-normal text-gray-500">The Defi Oracle Meta Explorer</span>
</div>
</Link>
<div className="flex gap-4">
<Link href="/blocks" className="text-gray-700 dark:text-gray-300 hover:text-primary-600">
Blocks
</Link>
<Link href="/transactions" className="text-gray-700 dark:text-gray-300 hover:text-primary-600">
Transactions
</Link>
<Link href="/search" className="text-gray-700 dark:text-gray-300 hover:text-primary-600">
Search
</Link>
<Link href="/wallet" className="text-gray-700 dark:text-gray-300 hover:text-primary-600">
Wallet
</Link>
</div>
</div>
</div>
</div>
</nav>
<Navbar />
<main>{children}</main>
</body>
</html>

View File

@@ -0,0 +1,166 @@
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useState } from 'react'
const navLink = 'text-gray-700 dark:text-gray-300 hover:text-primary-600 dark:hover:text-primary-400 transition-colors'
const navLinkActive = 'text-primary-600 dark:text-primary-400 font-medium'
function NavDropdown({
label,
icon,
children,
}: {
label: string
icon: React.ReactNode
children: React.ReactNode
}) {
const [open, setOpen] = useState(false)
return (
<div
className="relative"
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
>
<button
type="button"
className={`flex items-center gap-1.5 px-3 py-2 rounded-md ${navLink}`}
onClick={() => setOpen((v) => !v)}
aria-expanded={open}
aria-haspopup="true"
>
{icon}
<span>{label}</span>
<svg className={`w-3.5 h-3.5 transition-transform ${open ? 'rotate-180' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{open && (
<ul
className="absolute left-0 top-full mt-1 min-w-[200px] rounded-lg bg-white dark:bg-gray-800 shadow-lg border border-gray-200 dark:border-gray-700 py-1 z-50"
role="menu"
>
{children}
</ul>
)}
</div>
)
}
function DropdownItem({
href,
icon,
children,
external,
}: {
href: string
icon?: React.ReactNode
children: React.ReactNode
external?: boolean
}) {
const className = `flex items-center gap-2 px-4 py-2.5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 ${navLink}`
if (external) {
return (
<li role="none">
<a href={href} target="_self" rel="noopener" className={className} role="menuitem">
{icon}
<span>{children}</span>
</a>
</li>
)
}
return (
<li role="none">
<Link href={href} className={className} role="menuitem">
{icon}
<span>{children}</span>
</Link>
</li>
)
}
export default function Navbar() {
const pathname = usePathname() ?? ''
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const [exploreOpen, setExploreOpen] = useState(false)
const [toolsOpen, setToolsOpen] = useState(false)
return (
<nav className="bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700">
<div className="container mx-auto px-4">
<div className="flex items-center justify-between h-16">
<div className="flex items-center gap-4 md:gap-8">
<Link href="/" className="text-xl font-bold text-primary-600 dark:text-primary-400 flex flex-col" onClick={() => setMobileMenuOpen(false)}>
<span>SolaceScanScout</span>
<span className="text-xs font-normal text-gray-500 dark:text-gray-400">The Defi Oracle Meta Explorer</span>
</Link>
<div className="hidden md:flex items-center gap-1">
<NavDropdown
label="Explore"
icon={<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0h.5a2.5 2.5 0 002.5-2.5V3.935M12 12a2 2 0 104 0 2 2 0 00-4 0z" /></svg>}
>
<DropdownItem href="/" icon={<span className="text-gray-400"></span>}>Home</DropdownItem>
<DropdownItem href="/blocks" icon={<span className="text-gray-400"></span>}>Blocks</DropdownItem>
<DropdownItem href="/transactions" icon={<span className="text-gray-400"></span>}>Transactions</DropdownItem>
</NavDropdown>
<NavDropdown
label="Tools"
icon={<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>}
>
<DropdownItem href="/search">Search</DropdownItem>
<DropdownItem href="/wallet">Wallet</DropdownItem>
</NavDropdown>
</div>
</div>
<div className="flex items-center md:hidden">
<button
type="button"
className="p-2 rounded-md text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
onClick={() => setMobileMenuOpen((o) => !o)}
aria-expanded={mobileMenuOpen}
aria-label="Toggle menu"
>
{mobileMenuOpen ? (
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
) : (
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" /></svg>
)}
</button>
</div>
</div>
{mobileMenuOpen && (
<div className="md:hidden py-3 border-t border-gray-200 dark:border-gray-700">
<div className="flex flex-col gap-1">
<Link href="/" className={`px-3 py-2.5 rounded-md ${pathname === '/' ? navLinkActive : navLink}`} onClick={() => setMobileMenuOpen(false)}>Home</Link>
<div className="relative">
<button type="button" className={`flex items-center justify-between w-full px-3 py-2.5 rounded-md ${navLink}`} onClick={() => setExploreOpen((o) => !o)} aria-expanded={exploreOpen}>
<span>Explore</span>
<svg className={`w-4 h-4 transition-transform ${exploreOpen ? 'rotate-180' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
</button>
{exploreOpen && (
<ul className="pl-4 mt-1 space-y-0.5">
<li><Link href="/blocks" className={`block px-3 py-2 rounded-md ${navLink}`} onClick={() => setMobileMenuOpen(false)}>Blocks</Link></li>
<li><Link href="/transactions" className={`block px-3 py-2 rounded-md ${navLink}`} onClick={() => setMobileMenuOpen(false)}>Transactions</Link></li>
</ul>
)}
</div>
<div className="relative">
<button type="button" className={`flex items-center justify-between w-full px-3 py-2.5 rounded-md ${navLink}`} onClick={() => setToolsOpen((o) => !o)} aria-expanded={toolsOpen}>
<span>Tools</span>
<svg className={`w-4 h-4 transition-transform ${toolsOpen ? 'rotate-180' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
</button>
{toolsOpen && (
<ul className="pl-4 mt-1 space-y-0.5">
<li><Link href="/search" className={`block px-3 py-2 rounded-md ${navLink}`} onClick={() => setMobileMenuOpen(false)}>Search</Link></li>
<li><Link href="/wallet" className={`block px-3 py-2 rounded-md ${navLink}`} onClick={() => setMobileMenuOpen(false)}>Wallet</Link></li>
</ul>
)}
</div>
</div>
</div>
)}
</div>
</nav>
)
}