223 lines
8.8 KiB
TypeScript
223 lines
8.8 KiB
TypeScript
"use client";
|
|
import Link from "next/link";
|
|
import { usePathname, useRouter } from "next/navigation";
|
|
import { Disc3, Search, FolderHeart, Tag, Settings, Trash2, Users, Building2, Film, Database, ChevronDown, Layers } from "lucide-react";
|
|
import { BRAND } from "@/lib/brand";
|
|
import { useSettingsPanel } from "@/components/settings/SettingsPanelProvider";
|
|
import { useTrashPanel } from "@/components/trash/TrashPanelProvider";
|
|
import { QueueIndicator } from "@/components/queue/QueueIndicator";
|
|
import { cn } from "@/lib/utils";
|
|
import { useEffect, useRef, useState } from "react";
|
|
|
|
export function TopNav() {
|
|
const pathname = usePathname();
|
|
const router = useRouter();
|
|
const { open: settingsOpen, toggle: toggleSettings } = useSettingsPanel();
|
|
const { open: trashOpen, toggle: toggleTrash } = useTrashPanel();
|
|
const [q, setQ] = useState("");
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
useEffect(() => {
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
|
e.preventDefault();
|
|
inputRef.current?.focus();
|
|
}
|
|
};
|
|
window.addEventListener("keydown", onKey);
|
|
return () => window.removeEventListener("keydown", onKey);
|
|
}, []);
|
|
|
|
const submit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
const trimmed = q.trim();
|
|
if (!trimmed) return;
|
|
router.push(`/search?q=${encodeURIComponent(trimmed)}`);
|
|
};
|
|
|
|
const link = (href: string, label: string, Icon: React.ComponentType<{ className?: string }>) => {
|
|
const active = pathname === href || (href !== "/" && pathname.startsWith(href));
|
|
return (
|
|
<Link
|
|
href={href}
|
|
className={cn(
|
|
"flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors w-[110px]",
|
|
active ? "text-[var(--color-fg)] bg-[var(--color-glass-strong)]" : "text-[var(--color-fg-dim)] hover:text-[var(--color-fg)]",
|
|
)}
|
|
>
|
|
<Icon className="w-4 h-4" />
|
|
<span>{label}</span>
|
|
</Link>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<header className="sticky top-0 z-40 border-b border-[var(--color-glass-border)] backdrop-blur-xl bg-[color-mix(in_oklch,var(--color-bg-0)_70%,transparent)]">
|
|
<div className="max-w-[1600px] mx-auto px-6 h-16 flex items-center gap-6">
|
|
<Link href="/" className="flex items-center gap-2 group">
|
|
<div className="relative">
|
|
<Disc3 className="w-5 h-5 text-[var(--color-cyan)] group-hover:rotate-90 transition-transform duration-700" />
|
|
<div className="absolute inset-0 blur-md bg-[var(--color-cyan)] opacity-50 -z-10" />
|
|
</div>
|
|
<span className="font-semibold tracking-tight text-gradient-accent text-lg">{BRAND.name}</span>
|
|
</Link>
|
|
|
|
<nav className="flex items-center gap-1">
|
|
{link("/", "Library", Disc3)}
|
|
{link("/actress", "Actress", Users)}
|
|
<DatabaseMenu pathname={pathname} />
|
|
{link("/category", "Categories", Layers)}
|
|
{link("/tag", "Tags", Tag)}
|
|
{link("/collection", "Collection", FolderHeart)}
|
|
</nav>
|
|
|
|
<div className="ml-auto flex items-center gap-2">
|
|
<form onSubmit={submit} className="w-full max-w-xs relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--color-fg-muted)] pointer-events-none" />
|
|
<input
|
|
ref={inputRef}
|
|
value={q}
|
|
onChange={(e) => setQ(e.target.value)}
|
|
placeholder="Search Code, Title, Notes…"
|
|
className="w-full glass rounded-xl pl-10 pr-16 py-2 text-sm outline-none focus:border-[var(--color-cyan)] focus:shadow-[var(--shadow-glow-cyan)] transition-all placeholder:text-[var(--color-fg-muted)]"
|
|
/>
|
|
<kbd className="absolute right-3 top-1/2 -translate-y-1/2 text-[10px] font-mono text-[var(--color-fg-muted)] glass px-1.5 py-0.5 rounded">
|
|
⌘K
|
|
</kbd>
|
|
</form>
|
|
|
|
<QueueIndicator />
|
|
|
|
<button
|
|
onClick={toggleTrash}
|
|
aria-label="Trash"
|
|
aria-pressed={trashOpen}
|
|
title="Trash"
|
|
className={cn(
|
|
"w-9 h-9 grid place-items-center rounded-lg border transition-colors shrink-0",
|
|
trashOpen
|
|
? "bg-[var(--color-coral)]/15 border-[var(--color-coral)]/40 text-[var(--color-coral)]"
|
|
: "border-[var(--color-glass-border)] text-[var(--color-fg-dim)] hover:text-[var(--color-fg)] hover:bg-[var(--color-glass)]",
|
|
)}
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
|
|
<button
|
|
onClick={toggleSettings}
|
|
aria-label="Settings"
|
|
aria-pressed={settingsOpen}
|
|
title="Settings"
|
|
className={cn(
|
|
"w-9 h-9 grid place-items-center rounded-lg border transition-colors shrink-0",
|
|
settingsOpen
|
|
? "bg-[var(--color-cyan)]/15 border-[var(--color-cyan)]/40 text-[var(--color-cyan)]"
|
|
: "border-[var(--color-glass-border)] text-[var(--color-fg-dim)] hover:text-[var(--color-fg)] hover:bg-[var(--color-glass)]",
|
|
)}
|
|
>
|
|
<Settings className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|
|
|
|
const DATABASE_ITEMS: Array<{ href: string; label: string; Icon: React.ComponentType<{ className?: string }> }> = [
|
|
{ href: "/studios", label: "Studios", Icon: Building2 },
|
|
{ href: "/series", label: "Series", Icon: Film },
|
|
];
|
|
|
|
function DatabaseMenu({ pathname }: { pathname: string }) {
|
|
const [open, setOpen] = useState(false);
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
// Small close-delay so moving the cursor from the trigger button into
|
|
// the dropdown (across the 1 px positioning offset) doesn't dismiss.
|
|
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
const active = DATABASE_ITEMS.some((it) => pathname === it.href || pathname.startsWith(it.href + "/"));
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const onClick = (e: MouseEvent) => {
|
|
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
|
};
|
|
const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setOpen(false); };
|
|
window.addEventListener("mousedown", onClick);
|
|
window.addEventListener("keydown", onKey);
|
|
return () => {
|
|
window.removeEventListener("mousedown", onClick);
|
|
window.removeEventListener("keydown", onKey);
|
|
};
|
|
}, [open]);
|
|
|
|
useEffect(() => {
|
|
return () => { if (closeTimer.current) clearTimeout(closeTimer.current); };
|
|
}, []);
|
|
|
|
const cancelClose = () => {
|
|
if (closeTimer.current) { clearTimeout(closeTimer.current); closeTimer.current = null; }
|
|
};
|
|
const scheduleClose = () => {
|
|
cancelClose();
|
|
closeTimer.current = setTimeout(() => setOpen(false), 120);
|
|
};
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className="relative"
|
|
onMouseEnter={() => { cancelClose(); setOpen(true); }}
|
|
onMouseLeave={scheduleClose}
|
|
>
|
|
<button
|
|
type="button"
|
|
onClick={() => setOpen((s) => !s)}
|
|
aria-haspopup="menu"
|
|
aria-expanded={open}
|
|
className={cn(
|
|
"flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors w-[110px]",
|
|
active || open
|
|
? "text-[var(--color-fg)] bg-[var(--color-glass-strong)]"
|
|
: "text-[var(--color-fg-dim)] hover:text-[var(--color-fg)]",
|
|
)}
|
|
>
|
|
<Database className="w-4 h-4" />
|
|
<span>Database</span>
|
|
<ChevronDown className={cn("w-3 h-3 transition-transform", open && "rotate-180")} />
|
|
</button>
|
|
|
|
{open && (
|
|
<div
|
|
role="menu"
|
|
// pt-1 adds a hover bridge so the cursor can cross from the
|
|
// trigger to the menu items without leaving the wrapper.
|
|
className="absolute top-full left-0 pt-1 min-w-[160px] z-50"
|
|
>
|
|
<div className="rounded-lg border border-[var(--color-glass-border-strong)] bg-[var(--color-bg-0)] shadow-lg backdrop-blur-xl overflow-hidden">
|
|
{DATABASE_ITEMS.map(({ href, label, Icon }) => {
|
|
const itemActive = pathname === href || pathname.startsWith(href + "/");
|
|
return (
|
|
<Link
|
|
key={href}
|
|
href={href}
|
|
onClick={() => setOpen(false)}
|
|
role="menuitem"
|
|
className={cn(
|
|
"flex items-center gap-2 px-3 py-1.5 text-sm transition-colors",
|
|
itemActive
|
|
? "text-[var(--color-cyan)] bg-[var(--color-glass)]"
|
|
: "text-[var(--color-fg-dim)] hover:text-[var(--color-fg)] hover:bg-[var(--color-glass)]",
|
|
)}
|
|
>
|
|
<Icon className="w-4 h-4" />
|
|
{label}
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|