Files
chat-website/src/components/layout/Navbar.tsx
T

160 lines
4.9 KiB
TypeScript

'use client';
import { useState, useEffect, useRef } from 'react';
import Link from 'next/link';
import { useTranslations } from 'next-intl';
import { useSession, signIn, signOut } from 'next-auth/react';
import { LocaleSwitcher } from '@/components/layout/LocaleSwitcher';
import { ThemeSwitcher } from '@/components/layout/ThemeSwitcher';
import HamburgerButton from '@/components/layout/HamburgerButton';
import AuthProfileButton from '@/components/layout/AuthProfileButton';
import ScrollProgressBar from '@/components/layout/ScrollProgressBar';
import Logo from '@/components/layout/Logo';
export default function Navbar() {
const t = useTranslations('nav');
const [open, setOpen] = useState(false);
const [profileOpen, setProfileOpen] = useState(false);
const menuRef = useRef<HTMLDivElement | null>(null);
const burgerRef = useRef<HTMLButtonElement | null>(null);
const profileRef = useRef<HTMLDivElement | null>(null);
const { data: session, status } = useSession();
const authenticated =
status === 'authenticated' && session?.error !== 'RefreshAccessTokenError';
// const username = session?.user?.name || session?.user?.email;
// const roles = (session as any)?.roles || [];
const links = [
{ to: '/', label: t('label1') },
{ to: '/example', label: t('label2') },
{ to: '/doc', label: t('label3') },
];
useEffect(() => {
if (session?.error === 'RefreshAccessTokenError') {
signOut({ callbackUrl: '/' });
}
}, [session]);
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
const clickedBurger = burgerRef.current?.contains(e.target as Node);
const clickedProfile = profileRef.current?.contains(e.target as Node);
const clickedMenu = menuRef.current?.contains(e.target as Node);
const clickedInsideUI = clickedBurger || clickedProfile || clickedMenu;
if (!clickedInsideUI) {
setOpen(false);
setProfileOpen(false);
}
};
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
}, [open, profileOpen]);
const toggleProfile = () => {
setProfileOpen((v) => {
const next = !v;
if (next) setOpen(false);
return next;
});
};
const toggleBurger = () => {
setOpen((v) => {
const next = !v;
if (next) setProfileOpen(false);
return next;
});
};
return (
<>
<nav className="fixed top-0 w-full z-50 h-[var(--header-height)] bg-primary text-primary transition-theme">
<div className="w-full flex justify-between items-center px-4 sm:px-6 py-4 transition-theme">
{/* Logo */}
<Link
href="/"
className="flex items-center gap-1 sm:gap-2 font-bold text-accent text-sm sm:text-base md:text-lg"
>
<Logo />
</Link>
{/* Desktop Links */}
<ul className="hidden md:flex gap-8 transition-theme">
{links.map((link) => (
<li key={link.to}>
<Link href={link.to} className="hover:opacity-80">
{link.label}
</Link>
</li>
))}
</ul>
{/* Right actions */}
<div className="flex items-center">
<AuthProfileButton
authenticated={authenticated}
profileOpen={profileOpen}
setProfileOpen={setProfileOpen}
profileRef={profileRef}
signIn={signIn}
signOut={signOut}
toggleProfile={toggleProfile}
/>
{/* Hamburger */}
<HamburgerButton
open={open}
toggleBurger={toggleBurger}
burgerRef={burgerRef}
/>
</div>
</div>
<ScrollProgressBar />
</nav>
{/* Dropdown menu */}
<div
ref={menuRef}
className={`
fixed top-[var(--header-height)] left-0 z-40
w-full md:w-64 md:left-auto md:right-6
origin-top transition-all duration-100
${open ? 'opacity-100 scale-y-100' : 'opacity-0 scale-y-0 pointer-events-none'}
`}
>
<div className="overflow-hidden bg-primary border border-border md:rounded-xl md:shadow-lg transition-theme">
<ul className="flex flex-col px-6 py-4 gap-4 transition-theme">
{links.map((link) => (
<li key={link.to}>
<Link
className="hover:text-accent hover:opacity-80"
href={link.to}
onClick={() => setOpen(false)}
>
{link.label}
</Link>
</li>
))}
{/* Theme + Language switch */}
<li className="pt-2 border-t border-border">
<LocaleSwitcher />
<ThemeSwitcher />
</li>
</ul>
</div>
</div>
</>
);
}