first commit
Made-with: Cursor
This commit is contained in:
300
frontend/components/public/Navbar.tsx
Normal file
300
frontend/components/public/Navbar.tsx
Normal file
@@ -0,0 +1,300 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user