2026-02-07 14:20:26 +01:00
|
|
|
import { useState } from 'react'
|
2026-02-17 20:48:42 +01:00
|
|
|
import { Link, Outlet, useLocation } from 'react-router-dom'
|
2026-02-20 19:45:12 +01:00
|
|
|
import { useTheme } from '../hooks/useTheme'
|
2026-02-17 20:48:42 +01:00
|
|
|
|
|
|
|
|
const navLinks = [
|
|
|
|
|
{ to: '/runs/new', label: 'New Run' },
|
|
|
|
|
{ to: '/runs', label: 'My Runs' },
|
|
|
|
|
{ to: '/genlockes', label: 'Genlockes' },
|
|
|
|
|
{ to: '/stats', label: 'Stats' },
|
|
|
|
|
{ to: '/admin', label: 'Admin' },
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
function NavLink({
|
|
|
|
|
to,
|
|
|
|
|
active,
|
|
|
|
|
children,
|
|
|
|
|
onClick,
|
|
|
|
|
className = '',
|
|
|
|
|
}: {
|
|
|
|
|
to: string
|
|
|
|
|
active: boolean
|
|
|
|
|
children: React.ReactNode
|
|
|
|
|
onClick?: () => void
|
|
|
|
|
className?: string
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<Link
|
|
|
|
|
to={to}
|
|
|
|
|
onClick={onClick}
|
|
|
|
|
className={`${className} px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
|
|
|
|
active
|
|
|
|
|
? 'bg-accent-600/20 text-accent-300'
|
|
|
|
|
: 'text-text-secondary hover:text-text-primary hover:bg-surface-3'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</Link>
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-02-04 17:13:58 +01:00
|
|
|
|
2026-02-20 19:45:12 +01:00
|
|
|
function ThemeToggle() {
|
|
|
|
|
const { theme, toggle } = useTheme()
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={toggle}
|
|
|
|
|
className="p-2 rounded-md text-text-secondary hover:text-text-primary hover:bg-surface-3 transition-colors"
|
|
|
|
|
aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
|
|
|
|
|
>
|
|
|
|
|
{theme === 'dark' ? (
|
|
|
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
strokeWidth={2}
|
|
|
|
|
d="M12 3v1m0 16v1m8.66-13.66l-.71.71M4.05 19.95l-.71.71M21 12h-1M4 12H3m16.66 7.66l-.71-.71M4.05 4.05l-.71-.71M16 12a4 4 0 11-8 0 4 4 0 018 0z"
|
|
|
|
|
/>
|
|
|
|
|
</svg>
|
|
|
|
|
) : (
|
|
|
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
strokeWidth={2}
|
|
|
|
|
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
|
|
|
|
|
/>
|
|
|
|
|
</svg>
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 17:13:58 +01:00
|
|
|
export function Layout() {
|
2026-02-07 14:20:26 +01:00
|
|
|
const [menuOpen, setMenuOpen] = useState(false)
|
2026-02-17 20:48:42 +01:00
|
|
|
const location = useLocation()
|
|
|
|
|
|
|
|
|
|
function isActive(to: string) {
|
|
|
|
|
if (to === '/runs/new') return location.pathname === '/runs/new'
|
|
|
|
|
return location.pathname.startsWith(to)
|
|
|
|
|
}
|
2026-02-07 14:20:26 +01:00
|
|
|
|
2026-02-04 17:13:58 +01:00
|
|
|
return (
|
2026-02-17 20:48:42 +01:00
|
|
|
<div className="min-h-screen flex flex-col bg-surface-0 text-text-primary">
|
|
|
|
|
<nav className="sticky top-0 z-40 bg-surface-1/80 backdrop-blur-lg border-b border-border-default">
|
2026-02-04 17:13:58 +01:00
|
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
2026-02-17 20:48:42 +01:00
|
|
|
<div className="flex justify-between h-14">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Link to="/" className="flex items-center gap-2 group">
|
|
|
|
|
<img
|
|
|
|
|
src="/favicon.svg"
|
|
|
|
|
alt=""
|
|
|
|
|
className="w-7 h-7 transition-transform group-hover:scale-110"
|
|
|
|
|
/>
|
|
|
|
|
<span className="text-lg font-bold tracking-tight text-text-primary">ANT</span>
|
2026-02-04 17:13:58 +01:00
|
|
|
</Link>
|
|
|
|
|
</div>
|
2026-02-07 14:20:26 +01:00
|
|
|
{/* Desktop nav */}
|
2026-02-17 20:48:42 +01:00
|
|
|
<div className="hidden sm:flex items-center gap-1">
|
|
|
|
|
{navLinks.map((link) => (
|
|
|
|
|
<NavLink key={link.to} to={link.to} active={isActive(link.to)}>
|
|
|
|
|
{link.label}
|
|
|
|
|
</NavLink>
|
|
|
|
|
))}
|
2026-02-20 19:45:12 +01:00
|
|
|
<ThemeToggle />
|
2026-02-04 17:13:58 +01:00
|
|
|
</div>
|
2026-02-07 14:20:26 +01:00
|
|
|
{/* Mobile hamburger */}
|
2026-02-20 19:45:12 +01:00
|
|
|
<div className="flex items-center gap-1 sm:hidden">
|
|
|
|
|
<ThemeToggle />
|
2026-02-07 14:20:26 +01:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setMenuOpen(!menuOpen)}
|
2026-02-17 20:48:42 +01:00
|
|
|
className="p-2 rounded-md text-text-secondary hover:text-text-primary hover:bg-surface-3 transition-colors"
|
2026-02-07 14:20:26 +01:00
|
|
|
aria-label="Toggle menu"
|
|
|
|
|
>
|
2026-02-17 20:48:42 +01:00
|
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
2026-02-07 14:20:26 +01:00
|
|
|
{menuOpen ? (
|
|
|
|
|
<path
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
strokeWidth={2}
|
|
|
|
|
d="M6 18L18 6M6 6l12 12"
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<path
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
strokeWidth={2}
|
|
|
|
|
d="M4 6h16M4 12h16M4 18h16"
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2026-02-04 17:13:58 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-02-07 14:20:26 +01:00
|
|
|
{/* Mobile dropdown */}
|
|
|
|
|
{menuOpen && (
|
2026-02-17 20:48:42 +01:00
|
|
|
<div className="sm:hidden border-t border-border-default">
|
2026-02-07 14:20:26 +01:00
|
|
|
<div className="px-2 pt-2 pb-3 space-y-1">
|
2026-02-17 20:48:42 +01:00
|
|
|
{navLinks.map((link) => (
|
|
|
|
|
<NavLink
|
|
|
|
|
key={link.to}
|
|
|
|
|
to={link.to}
|
|
|
|
|
active={isActive(link.to)}
|
|
|
|
|
onClick={() => setMenuOpen(false)}
|
|
|
|
|
className="block"
|
|
|
|
|
>
|
|
|
|
|
{link.label}
|
|
|
|
|
</NavLink>
|
|
|
|
|
))}
|
2026-02-07 14:20:26 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-02-04 17:13:58 +01:00
|
|
|
</nav>
|
2026-02-13 13:59:18 +01:00
|
|
|
<main className="flex-1">
|
2026-02-04 17:13:58 +01:00
|
|
|
<Outlet />
|
|
|
|
|
</main>
|
2026-02-17 20:48:42 +01:00
|
|
|
<footer className="border-t border-border-default bg-surface-1/50">
|
|
|
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 text-center text-xs text-text-tertiary">
|
|
|
|
|
Encounter data from{' '}
|
2026-02-10 16:01:14 +01:00
|
|
|
<a
|
|
|
|
|
href="https://pokedb.org"
|
2026-02-17 20:48:42 +01:00
|
|
|
className="underline hover:text-text-secondary transition-colors"
|
2026-02-10 16:01:14 +01:00
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
>
|
|
|
|
|
PokeDB.org
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
</footer>
|
2026-02-04 17:13:58 +01:00
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|