186 lines
6.8 KiB
TypeScript
186 lines
6.8 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import GroupDropdown from "@/components/group-dropdown";
|
|
import { useAuthContext } from "@/hooks/auth-context";
|
|
import { useGroupsContext } from "@/hooks/groups-context";
|
|
|
|
export default function Navbar() {
|
|
const router = useRouter();
|
|
const { checkSession, logout } = useAuthContext();
|
|
const { activeGroupId } = useGroupsContext();
|
|
const [inviteModalCode, setInviteModalCode] = useState<string | null>(null);
|
|
const [inviteCopied, setInviteCopied] = useState(false);
|
|
const [hideNavbar, setHideNavbar] = useState(false);
|
|
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
|
const [userEmail, setUserEmail] = useState<string | null>(null);
|
|
const userMenuRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
async function handleCopyInvite() {
|
|
if (!inviteModalCode) return;
|
|
|
|
await navigator.clipboard.writeText(inviteModalCode);
|
|
setInviteCopied(true);
|
|
}
|
|
|
|
async function handleLogout() {
|
|
await logout();
|
|
setUserMenuOpen(false);
|
|
router.push("/login");
|
|
}
|
|
|
|
useEffect(() => {
|
|
let active = true;
|
|
async function loadUser() {
|
|
const user = await checkSession();
|
|
if (active) setUserEmail(user?.email || null);
|
|
}
|
|
loadUser();
|
|
return () => {
|
|
active = false;
|
|
};
|
|
}, [checkSession]);
|
|
|
|
useEffect(() => {
|
|
function handleClickOutside(event: MouseEvent) {
|
|
if (!userMenuOpen || !userMenuRef.current) return;
|
|
if (!userMenuRef.current.contains(event.target as Node))
|
|
setUserMenuOpen(false);
|
|
}
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
}, [userMenuOpen]);
|
|
|
|
useEffect(() => {
|
|
if (!inviteModalCode) return;
|
|
function handleKeyDown(event: KeyboardEvent) {
|
|
if (event.key === "Escape") setInviteModalCode(null);
|
|
}
|
|
window.addEventListener("keydown", handleKeyDown);
|
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
}, [inviteModalCode]);
|
|
|
|
useEffect(() => {
|
|
let lastY = window.scrollY;
|
|
function onScroll() {
|
|
const current = window.scrollY;
|
|
const diff = current - lastY;
|
|
if (Math.abs(diff) < 6) return;
|
|
if (current <= 8) {
|
|
setHideNavbar(false);
|
|
} else if (diff > 0) {
|
|
setHideNavbar(true);
|
|
} else {
|
|
setHideNavbar(false);
|
|
}
|
|
lastY = current;
|
|
}
|
|
window.addEventListener("scroll", onScroll, { passive: true });
|
|
return () => window.removeEventListener("scroll", onScroll);
|
|
}, []);
|
|
|
|
return (
|
|
<>
|
|
<header className={`sticky top-0 z-40 border-b border-accent-weak bg-app backdrop-blur transition-transform duration-200 ${hideNavbar ? "-translate-y-full" : "translate-y-0"}`}>
|
|
<div className="mx-auto flex max-w-5xl items-center justify-between px-4 py-2.5">
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
type="button"
|
|
className="flex h-9 w-9 items-center justify-center overflow-hidden rounded-xl border border-accent bg-panel hover:border-accent-strong"
|
|
aria-label="Settings"
|
|
onClick={() => {
|
|
if (activeGroupId) router.push("/groups/settings");
|
|
else router.push("/");
|
|
}}
|
|
>
|
|
<img src="/icons/navbar-settings.png" alt="" className="h-full w-full rounded-lg object-cover" />
|
|
</button>
|
|
<div className="hidden sm:block text-sm font-semibold">Fiddy</div>
|
|
</div>
|
|
|
|
<GroupDropdown
|
|
onInviteCode={code => {
|
|
setInviteModalCode(code);
|
|
setInviteCopied(false);
|
|
}}
|
|
/>
|
|
|
|
<div className="relative" ref={userMenuRef}>
|
|
<button
|
|
type="button"
|
|
aria-label="User menu"
|
|
onClick={() => setUserMenuOpen(prev => !prev)}
|
|
className="flex h-9 w-9 items-center justify-center rounded-full border border-accent-weak bg-panel text-sm text-muted hover:border-accent"
|
|
>
|
|
<span className="text-base">👤</span>
|
|
</button>
|
|
{userMenuOpen ? (
|
|
<div className="absolute right-0 mt-2 w-48 rounded-xl border border-accent-weak bg-panel p-3 text-sm shadow-lg">
|
|
<div className="text-xs text-soft">Signed in as</div>
|
|
<div className="mt-1 truncate text-sm font-semibold text-[color:var(--color-text)]">
|
|
{userEmail || "User"}
|
|
</div>
|
|
<div className="my-3 h-px divider" />
|
|
<button
|
|
type="button"
|
|
className="w-full rounded-lg btn-outline-accent px-3 py-2 text-xs font-semibold"
|
|
onClick={() => {
|
|
setUserMenuOpen(false);
|
|
router.push("/settings");
|
|
}}
|
|
>
|
|
Settings
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="mt-2 w-full rounded-lg border border-red-400/60 bg-red-500/10 px-3 py-2 text-xs font-semibold text-red-200"
|
|
onClick={handleLogout}
|
|
>
|
|
Logout
|
|
</button>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
{inviteModalCode ? (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4 py-6 overflow-y-auto">
|
|
<div
|
|
className="w-full max-w-sm rounded-2xl border border-accent-weak bg-panel p-5 shadow-xl max-h-[90vh]"
|
|
onKeyDown={event => {
|
|
if (event.key === "Escape") setInviteModalCode(null);
|
|
}}
|
|
role="dialog"
|
|
tabIndex={-1}
|
|
>
|
|
<div className="text-lg font-semibold">Invite code</div>
|
|
<p className="mt-2 text-sm text-muted">
|
|
Share this code to invite members. You can view it later in group settings.
|
|
</p>
|
|
<div className="mt-4 rounded-lg border border-accent-weak bg-surface px-3 py-2 text-center text-lg tracking-widest">
|
|
{inviteModalCode}
|
|
</div>
|
|
<div className="mt-4 flex items-center gap-2">
|
|
<button
|
|
type="button"
|
|
className="flex-1 rounded-lg btn-accent px-3 py-2 text-sm font-semibold"
|
|
onClick={handleCopyInvite}
|
|
>
|
|
{inviteCopied ? "Copied" : "Copy code"}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="flex-1 rounded-lg btn-outline-accent px-3 py-2 text-sm font-semibold"
|
|
onClick={() => setInviteModalCode(null)}
|
|
>
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</>
|
|
);
|
|
}
|