Files
BelgianBitcoinEmbassy/frontend/components/public/Navbar.tsx
Michilis 76210db03d first commit
Made-with: Cursor
2026-04-01 02:46:53 +00:00

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>
);
}