fiddy/apps/web/features/app-shell/components/navbar.tsx

186 lines
6.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import GroupDropdown from "@/features/groups/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}
</>
);
}