301 lines
9.9 KiB
TypeScript
301 lines
9.9 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useRef, useEffect } from "react";
|
|
import { usePathname, useRouter } from "next/navigation";
|
|
import Link from "next/link";
|
|
import Image from "next/image";
|
|
import { Menu, X, LogIn, User, LayoutDashboard, LogOut, Shield } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import { Button } from "@/components/ui/Button";
|
|
import { useAuth } from "@/hooks/useAuth";
|
|
import { shortenPubkey } from "@/lib/nostr";
|
|
|
|
const SECTION_LINKS = [{ label: "About", anchor: "about" }];
|
|
|
|
const PAGE_LINKS = [
|
|
{ label: "Meetups", href: "/events" },
|
|
{ label: "Community", href: "/community" },
|
|
{ label: "FAQ", href: "/faq" },
|
|
];
|
|
|
|
function ProfileAvatar({
|
|
picture,
|
|
name,
|
|
size = 36,
|
|
}: {
|
|
picture?: string;
|
|
name?: string;
|
|
size?: number;
|
|
}) {
|
|
const [imgError, setImgError] = useState(false);
|
|
const initial = (name || "?")[0].toUpperCase();
|
|
|
|
if (picture && !imgError) {
|
|
return (
|
|
<Image
|
|
src={picture}
|
|
alt={name || "Profile"}
|
|
width={size}
|
|
height={size}
|
|
className="rounded-full object-cover"
|
|
style={{ width: size, height: size }}
|
|
onError={() => setImgError(true)}
|
|
unoptimized
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="rounded-full bg-surface-container-high flex items-center justify-center text-on-surface font-bold text-sm"
|
|
style={{ width: size, height: size }}
|
|
>
|
|
{initial}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function Navbar() {
|
|
const [open, setOpen] = useState(false);
|
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
const pathname = usePathname();
|
|
const router = useRouter();
|
|
const { user, loading, logout } = useAuth();
|
|
const isHome = pathname === "/";
|
|
|
|
useEffect(() => {
|
|
function handleClickOutside(e: MouseEvent) {
|
|
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
|
|
setDropdownOpen(false);
|
|
}
|
|
}
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
}, []);
|
|
|
|
function sectionHref(anchor: string) {
|
|
return isHome ? `#${anchor}` : `/#${anchor}`;
|
|
}
|
|
|
|
const displayName = user?.name || user?.displayName || shortenPubkey(user?.pubkey || "");
|
|
const isStaff = user?.role === "ADMIN" || user?.role === "MODERATOR";
|
|
|
|
function handleLogout() {
|
|
setDropdownOpen(false);
|
|
setOpen(false);
|
|
logout();
|
|
router.push("/");
|
|
}
|
|
|
|
return (
|
|
<nav className="sticky top-0 z-50 bg-surface/95 backdrop-blur-md">
|
|
<div className="flex justify-between items-center max-w-7xl mx-auto px-8 h-20">
|
|
<Link
|
|
href="/"
|
|
className="text-xl font-bold text-primary-container tracking-[-0.02em]"
|
|
>
|
|
Belgian Bitcoin Embassy
|
|
</Link>
|
|
|
|
<div className="hidden md:flex space-x-10 items-center">
|
|
{SECTION_LINKS.map((link) => (
|
|
<a
|
|
key={link.anchor}
|
|
href={sectionHref(link.anchor)}
|
|
className="font-medium tracking-tight transition-colors duration-200 text-white/70 hover:text-primary"
|
|
>
|
|
{link.label}
|
|
</a>
|
|
))}
|
|
{PAGE_LINKS.map((link) => (
|
|
<Link
|
|
key={link.href}
|
|
href={link.href}
|
|
className={cn(
|
|
"font-medium tracking-tight transition-colors duration-200",
|
|
pathname.startsWith(link.href)
|
|
? "text-primary font-bold"
|
|
: "text-white/70 hover:text-primary"
|
|
)}
|
|
>
|
|
{link.label}
|
|
</Link>
|
|
))}
|
|
<Link
|
|
href="/blog"
|
|
className={cn(
|
|
"font-medium tracking-tight transition-colors duration-200",
|
|
pathname.startsWith("/blog")
|
|
? "text-primary font-bold"
|
|
: "text-white/70 hover:text-primary"
|
|
)}
|
|
>
|
|
Blog
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="hidden md:block">
|
|
{loading ? (
|
|
<div className="w-24 h-10" />
|
|
) : user ? (
|
|
<div className="relative" ref={dropdownRef}>
|
|
<button
|
|
onClick={() => setDropdownOpen(!dropdownOpen)}
|
|
className="flex items-center gap-3 px-3 py-1.5 rounded-lg transition-colors hover:bg-surface-container-high"
|
|
>
|
|
<ProfileAvatar
|
|
picture={user.picture}
|
|
name={user.name || user.displayName}
|
|
size={32}
|
|
/>
|
|
<span className="text-sm font-medium text-on-surface max-w-[120px] truncate">
|
|
{displayName}
|
|
</span>
|
|
</button>
|
|
|
|
{dropdownOpen && (
|
|
<div className="absolute right-0 mt-2 w-52 bg-surface-container-high rounded-xl py-2 shadow-lg shadow-black/30">
|
|
<Link
|
|
href="/dashboard"
|
|
onClick={() => setDropdownOpen(false)}
|
|
className="flex items-center gap-3 px-4 py-2.5 text-sm text-on-surface hover:bg-surface-bright transition-colors"
|
|
>
|
|
<LayoutDashboard size={16} className="text-on-surface-variant" />
|
|
Dashboard
|
|
</Link>
|
|
{isStaff && (
|
|
<Link
|
|
href="/admin"
|
|
onClick={() => setDropdownOpen(false)}
|
|
className="flex items-center gap-3 px-4 py-2.5 text-sm text-on-surface hover:bg-surface-bright transition-colors"
|
|
>
|
|
<Shield size={16} className="text-on-surface-variant" />
|
|
Admin
|
|
</Link>
|
|
)}
|
|
<button
|
|
onClick={handleLogout}
|
|
className="flex items-center gap-3 px-4 py-2.5 text-sm text-on-surface hover:bg-surface-bright transition-colors w-full text-left"
|
|
>
|
|
<LogOut size={16} className="text-on-surface-variant" />
|
|
Logout
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<Link href="/login">
|
|
<Button variant="primary" size="md">
|
|
<span className="flex items-center gap-2">
|
|
<LogIn size={16} />
|
|
Login
|
|
</span>
|
|
</Button>
|
|
</Link>
|
|
)}
|
|
</div>
|
|
|
|
<button
|
|
className="md:hidden text-on-surface"
|
|
onClick={() => setOpen(!open)}
|
|
aria-label="Toggle menu"
|
|
>
|
|
{open ? <X size={24} /> : <Menu size={24} />}
|
|
</button>
|
|
</div>
|
|
|
|
{open && (
|
|
<div className="md:hidden bg-surface-container px-8 pb-6 space-y-4">
|
|
{SECTION_LINKS.map((link) => (
|
|
<a
|
|
key={link.anchor}
|
|
href={sectionHref(link.anchor)}
|
|
onClick={() => setOpen(false)}
|
|
className="block py-2 font-medium tracking-tight transition-colors text-white/70 hover:text-primary"
|
|
>
|
|
{link.label}
|
|
</a>
|
|
))}
|
|
{PAGE_LINKS.map((link) => (
|
|
<Link
|
|
key={link.href}
|
|
href={link.href}
|
|
onClick={() => setOpen(false)}
|
|
className={cn(
|
|
"block py-2 font-medium tracking-tight transition-colors",
|
|
pathname.startsWith(link.href)
|
|
? "text-primary font-bold"
|
|
: "text-white/70 hover:text-primary"
|
|
)}
|
|
>
|
|
{link.label}
|
|
</Link>
|
|
))}
|
|
<Link
|
|
href="/blog"
|
|
onClick={() => setOpen(false)}
|
|
className={cn(
|
|
"block py-2 font-medium tracking-tight transition-colors",
|
|
pathname.startsWith("/blog")
|
|
? "text-primary font-bold"
|
|
: "text-white/70 hover:text-primary"
|
|
)}
|
|
>
|
|
Blog
|
|
</Link>
|
|
|
|
{loading ? null : user ? (
|
|
<>
|
|
<div className="flex items-center gap-3 pt-4">
|
|
<ProfileAvatar
|
|
picture={user.picture}
|
|
name={user.name || user.displayName}
|
|
size={32}
|
|
/>
|
|
<span className="text-sm font-medium text-on-surface truncate">
|
|
{displayName}
|
|
</span>
|
|
</div>
|
|
<Link
|
|
href="/dashboard"
|
|
onClick={() => setOpen(false)}
|
|
className="flex items-center gap-3 py-2 text-sm font-medium text-white/70 hover:text-primary transition-colors"
|
|
>
|
|
<LayoutDashboard size={16} />
|
|
Dashboard
|
|
</Link>
|
|
{isStaff && (
|
|
<Link
|
|
href="/admin"
|
|
onClick={() => setOpen(false)}
|
|
className="flex items-center gap-3 py-2 text-sm font-medium text-white/70 hover:text-primary transition-colors"
|
|
>
|
|
<Shield size={16} />
|
|
Admin
|
|
</Link>
|
|
)}
|
|
<button
|
|
onClick={handleLogout}
|
|
className="flex items-center gap-3 py-2 text-sm font-medium text-white/70 hover:text-primary transition-colors w-full"
|
|
>
|
|
<LogOut size={16} />
|
|
Logout
|
|
</button>
|
|
</>
|
|
) : (
|
|
<Link href="/login" onClick={() => setOpen(false)}>
|
|
<Button variant="primary" size="md" className="w-full mt-4">
|
|
<span className="flex items-center justify-center gap-2">
|
|
<LogIn size={16} />
|
|
Login
|
|
</span>
|
|
</Button>
|
|
</Link>
|
|
)}
|
|
</div>
|
|
)}
|
|
</nav>
|
|
);
|
|
}
|