fiddy/apps/web/components/navbar.tsx
Nico f8e426542d
Some checks failed
Build & Deploy Fiddy (Dokploy) / build (push) Has been cancelled
Build & Deploy Fiddy (Dokploy) / deploy (push) Has been cancelled
feat: implement schedules pivot, scheduler service, and dokploy deploy flow
2026-02-15 17:10:58 -08:00

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