|
|
"use client"; |
|
|
|
|
|
import { useEffect, useRef, useState } from "react"; |
|
|
import { Menu, Sun, Moon } from "lucide-react"; |
|
|
import Link from "next/link"; |
|
|
import Image from "next/image"; |
|
|
import { useRouter } from "next/navigation"; |
|
|
import { useTheme } from "./ThemeProvider"; |
|
|
import { Button } from "@/components/ui/button"; |
|
|
|
|
|
export default function Navbar() { |
|
|
const [isOpen, setIsOpen] = useState(false); |
|
|
const { theme, toggleTheme } = useTheme(); |
|
|
const router = useRouter(); |
|
|
|
|
|
const adminHoldTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); |
|
|
const adminGestureTriggeredRef = useRef(false); |
|
|
|
|
|
const clearAdminHoldTimer = () => { |
|
|
if (adminHoldTimerRef.current) { |
|
|
clearTimeout(adminHoldTimerRef.current); |
|
|
adminHoldTimerRef.current = null; |
|
|
} |
|
|
}; |
|
|
|
|
|
useEffect(() => { |
|
|
const onWindowBlur = () => { |
|
|
clearAdminHoldTimer(); |
|
|
adminGestureTriggeredRef.current = false; |
|
|
}; |
|
|
window.addEventListener("blur", onWindowBlur); |
|
|
return () => { |
|
|
window.removeEventListener("blur", onWindowBlur); |
|
|
}; |
|
|
|
|
|
}, []); |
|
|
|
|
|
return ( |
|
|
<nav className="fixed inset-x-0 top-0 z-50 bg-background/90 backdrop-blur supports-[backdrop-filter]:bg-background/70 shadow-[0_1px_0_rgba(255,255,255,0.03)]"> |
|
|
<div className="mx-auto flex h-16 max-w-6xl items-center justify-between px-4 sm:px-6"> |
|
|
<Link |
|
|
href="/" |
|
|
className="flex items-center gap-2.5" |
|
|
onPointerDown={(e) => { |
|
|
if (!e.ctrlKey) return; |
|
|
adminGestureTriggeredRef.current = false; |
|
|
clearAdminHoldTimer(); |
|
|
adminHoldTimerRef.current = setTimeout(() => { |
|
|
adminGestureTriggeredRef.current = true; |
|
|
router.push("/admin"); |
|
|
}, 3000); |
|
|
}} |
|
|
onPointerUp={() => { |
|
|
clearAdminHoldTimer(); |
|
|
}} |
|
|
onPointerLeave={() => { |
|
|
clearAdminHoldTimer(); |
|
|
}} |
|
|
onPointerCancel={() => { |
|
|
clearAdminHoldTimer(); |
|
|
}} |
|
|
onClick={(e) => { |
|
|
if (adminGestureTriggeredRef.current) { |
|
|
e.preventDefault(); |
|
|
adminGestureTriggeredRef.current = false; |
|
|
} |
|
|
}} |
|
|
> |
|
|
<Image |
|
|
src="/teich.svg" |
|
|
alt="TeichAI Logo" |
|
|
width={32} |
|
|
height={32} |
|
|
className="rounded-lg" |
|
|
/> |
|
|
<span className="text-lg font-semibold tracking-tight text-foreground">TeichAI Requests</span> |
|
|
</Link> |
|
|
|
|
|
<div className="hidden items-center gap-1 md:flex"> |
|
|
<Button |
|
|
onClick={toggleTheme} |
|
|
variant="ghost" |
|
|
size="icon" |
|
|
className="ml-1" |
|
|
aria-label="Toggle theme" |
|
|
> |
|
|
{theme === "dark" ? <Sun className="size-4" /> : <Moon className="size-4" />} |
|
|
</Button> |
|
|
|
|
|
<Button asChild className="ml-1"> |
|
|
<a href="https://huggingface.co/TeichAI" target="_blank" rel="noopener noreferrer"> |
|
|
HF Hub |
|
|
</a> |
|
|
</Button> |
|
|
</div> |
|
|
|
|
|
<div className="flex items-center gap-1 md:hidden"> |
|
|
<Button |
|
|
onClick={toggleTheme} |
|
|
variant="ghost" |
|
|
size="icon" |
|
|
aria-label="Toggle theme" |
|
|
> |
|
|
{theme === "dark" ? <Sun className="size-5" /> : <Moon className="size-5" />} |
|
|
</Button> |
|
|
|
|
|
<Button |
|
|
variant="ghost" |
|
|
size="icon" |
|
|
aria-label="Open menu" |
|
|
onClick={() => setIsOpen(!isOpen)} |
|
|
> |
|
|
<Menu className="size-5" /> |
|
|
</Button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
{isOpen && ( |
|
|
<div className="border-t border-border bg-background p-4 md:hidden"> |
|
|
<Button asChild className="w-full"> |
|
|
<a href="https://huggingface.co/TeichAI" target="_blank" rel="noopener noreferrer"> |
|
|
HF Hub |
|
|
</a> |
|
|
</Button> |
|
|
</div> |
|
|
)} |
|
|
</nav> |
|
|
); |
|
|
} |
|
|
|