"use client"; import { usePathname, useRouter } from "next/navigation"; import { useEffect, useMemo, useRef, useState } from "react"; import { PixelAvatar } from "@/components/pixel-avatar"; import { useUiPreferences } from "@/components/ui-preference-provider"; import { XpBar } from "@/components/xp-bar"; import { apiFetch } from "@/lib/api"; import { clearToken, readToken } from "@/lib/auth"; import type { ThemeId } from "@/themes/types"; type NavLink = { label: string; href: string; }; type NavGroup = { key: string; label: string; links: NavLink[]; }; type MeProfile = { id?: number; username?: string; }; function buildNavGroups(t: (key: string) => string, isAdmin: boolean): NavGroup[] { const groups: NavGroup[] = [ { key: "learn", label: t("nav.group.learn"), links: [ { label: t("nav.link.home"), href: "/" }, { label: t("nav.link.problems"), href: "/problems" }, { label: t("nav.link.submissions"), href: "/submissions" }, { label: t("nav.link.wrong_book"), href: "/wrong-book" }, { label: t("nav.link.kb"), href: "/kb" }, { label: t("nav.link.run"), href: "/run" }, ], }, { key: "contest", label: t("nav.group.contest"), links: [ { label: t("nav.link.contests"), href: "/contests" }, { label: t("nav.link.leaderboard"), href: "/leaderboard" }, ], }, { key: "account", label: t("nav.group.account"), links: [ { label: t("nav.link.auth"), href: "/auth" }, { label: t("nav.link.me"), href: "/me" }, ], }, ]; if (isAdmin) { groups.splice(2, 0, { key: "system", label: t("nav.group.system"), links: [ { label: t("nav.link.imports"), href: "/imports" }, { label: t("nav.link.backend_logs"), href: "/backend-logs" }, { label: t("nav.link.admin_users"), href: "/admin-users" }, { label: t("nav.link.admin_redeem"), href: "/admin-redeem" }, { label: t("nav.link.api_docs"), href: "/api-docs" }, ], }); } return groups; } function isActivePath(pathname: string, href: string): boolean { if (pathname === href) return true; if (href === "/") return pathname === "/"; return pathname.startsWith(`${href}/`); } function resolveActiveGroup(pathname: string, groups: NavGroup[]): string { for (const group of groups) { for (const item of group.links) { if (isActivePath(pathname, item.href)) return group.key; } } return groups[0]?.key ?? "learn"; } function resolveActiveLink(pathname: string, group: NavGroup): string { for (const item of group.links) { if (isActivePath(pathname, item.href)) return item.href; } return group.links[0]?.href ?? "/"; } export function AppNav() { const pathname = usePathname(); const router = useRouter(); const { theme, setTheme, language, setLanguage, themes, t } = useUiPreferences(); const directHttpAccessUrl = process.env.NEXT_PUBLIC_HTTP_ENTRY_URL?.trim() || "http://8.211.173.24:7888/"; const [hasToken, setHasToken] = useState(() => Boolean(readToken())); const [isAdmin, setIsAdmin] = useState(false); const [meProfile, setMeProfile] = useState(null); const [menuOpen, setMenuOpen] = useState(false); const [desktopOpenGroup, setDesktopOpenGroup] = useState(null); const desktopMenuRef = useRef(null); const navGroups = useMemo(() => buildNavGroups(t, isAdmin), [isAdmin, t]); const activeGroup = resolveActiveGroup(pathname, navGroups); const usePopupSecondary = theme === "default" || theme === "minecraft"; useEffect(() => { let canceled = false; const refresh = async () => { const token = readToken(); if (canceled) return; setHasToken(Boolean(token)); if (!token) { setIsAdmin(false); setMeProfile(null); return; } try { const me = await apiFetch("/api/v1/me", {}, token); if (!canceled) { setIsAdmin((me?.username ?? "") === "admin"); setMeProfile(me ?? null); } } catch { if (!canceled) { setIsAdmin(false); setMeProfile(null); } } }; const onRefresh = () => { void refresh(); }; void refresh(); window.addEventListener("storage", onRefresh); window.addEventListener("focus", onRefresh); return () => { canceled = true; window.removeEventListener("storage", onRefresh); window.removeEventListener("focus", onRefresh); }; }, [pathname]); useEffect(() => { if (!usePopupSecondary || !desktopOpenGroup) return; const onMouseDown = (event: MouseEvent) => { const target = event.target as Node | null; if (!target) return; if (desktopMenuRef.current?.contains(target)) return; setDesktopOpenGroup(null); }; window.addEventListener("mousedown", onMouseDown); return () => window.removeEventListener("mousedown", onMouseDown); }, [desktopOpenGroup, usePopupSecondary]); const currentGroup = navGroups.find((g) => g.key === activeGroup) ?? navGroups[0]; const avatarSeed = meProfile ? `${meProfile.username ?? "user"}-${meProfile.id ?? ""}` : "guest"; const handleLogout = () => { clearToken(); setHasToken(false); setIsAdmin(false); setMeProfile(null); setDesktopOpenGroup(null); }; return (
{t("nav.menu")}
{t("nav.link.http_ip_port")} {hasToken ? t("nav.logged_in") : t("nav.logged_out")} {hasToken && ( <> {theme === "minecraft" && (
)} )} {hasToken && ( )}
); }