popiart-server
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

150 lines
4.3 KiB

"use client";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { useEffect, useRef, useState, useTransition } from "react";
import type { PopiartUser } from "@/lib/popiart-api";
import type { Locale } from "@/lib/site-content";
function replaceLocale(pathname: string, locale: Locale) {
const parts = pathname.split("/");
if (parts[1] === "zh" || parts[1] === "en") {
parts[1] = locale;
return parts.join("/") || `/${locale}`;
}
return `/${locale}${pathname.startsWith("/") ? pathname : `/${pathname}`}`;
}
function userLabel(user: PopiartUser) {
return user.name || user.email || user.id;
}
function initials(user: PopiartUser) {
const source = user.name || user.email || user.id || "PA";
return source.slice(0, 2).toUpperCase();
}
export function HeaderControls({
locale,
user,
labels,
loginHref,
}: {
locale: Locale;
user: PopiartUser | null;
labels: {
zh: string;
en: string;
logout: string;
loggingOut: string;
loginHint: string;
login: string;
};
loginHref: string;
}) {
const pathname = usePathname();
const router = useRouter();
const [isPending, startTransition] = useTransition();
const [menuOpen, setMenuOpen] = useState(false);
const menuRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
function handlePointerDown(event: MouseEvent) {
if (!menuRef.current?.contains(event.target as Node)) {
setMenuOpen(false);
}
}
function handleEscape(event: KeyboardEvent) {
if (event.key === "Escape") {
setMenuOpen(false);
}
}
window.addEventListener("mousedown", handlePointerDown);
window.addEventListener("keydown", handleEscape);
return () => {
window.removeEventListener("mousedown", handlePointerDown);
window.removeEventListener("keydown", handleEscape);
};
}, []);
function switchLocale(target: Locale) {
setMenuOpen(false);
router.push(replaceLocale(pathname, target));
}
function logout() {
startTransition(async () => {
await fetch("/api/auth/logout", {
method: "POST",
});
router.refresh();
});
}
return (
<div className="header-actions">
<div className="locale-switch" ref={menuRef}>
<button
aria-expanded={menuOpen}
aria-haspopup="menu"
className={`locale-menu-button ${menuOpen ? "locale-menu-button-open" : ""}`}
onClick={() => setMenuOpen((open) => !open)}
type="button"
>
<span>{locale === "zh" ? labels.zh : labels.en}</span>
<span aria-hidden="true" className={`locale-menu-chevron ${menuOpen ? "locale-menu-chevron-open" : ""}`}>
</span>
</button>
{menuOpen ? (
<div className="locale-menu-list" role="menu">
<button
aria-pressed={locale === "zh"}
className={`locale-menu-item ${locale === "zh" ? "locale-menu-item-active" : ""}`}
onClick={() => switchLocale("zh")}
role="menuitemradio"
type="button"
>
{labels.zh}
</button>
<button
aria-pressed={locale === "en"}
className={`locale-menu-item ${locale === "en" ? "locale-menu-item-active" : ""}`}
onClick={() => switchLocale("en")}
role="menuitemradio"
type="button"
>
{labels.en}
</button>
</div>
) : null}
</div>
{user ? (
<div className="auth-cluster">
<Link className="session-pill" href={`/${locale}/console`} title={userLabel(user)}>
<span className="session-avatar">{initials(user)}</span>
<span className="session-name">{userLabel(user)}</span>
</Link>
<button
className="button button-light button-small button-auth-stable"
disabled={isPending}
onClick={logout}
type="button"
>
{isPending ? labels.loggingOut : labels.logout}
</button>
</div>
) : (
<div className="auth-cluster">
<Link className="button button-light button-small button-auth-stable" href={loginHref}>
{labels.login}
</Link>
</div>
)}
</div>
);
}