Files
web_hghl_h5/src/components/layout/mobile-menu.tsx
T
2026-10-28 17:07:32 +07:00

257 lines
9.2 KiB
TypeScript
Raw 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 Link from "next/link";
import { usePathname } from "next/navigation";
import { useEffect, useState, type ReactNode } from "react";
import { openAppDownload, useAppDownloadHref } from "@/components/download/download-dialog";
import { footerNavItems } from "@/components/layout/footer-nav-items";
import { lockBodyScroll } from "@/lib/browser/scroll-lock";
import type { AppTranslationInfo, ConfigInfo, TranslationCategory, TranslationInfo } from "@/lib/api/types";
import { OFFICIAL_TG_HANDLE, OFFICIAL_TG_URL } from "@/lib/seo/metadata";
import { resolveAboutPlatformHref, resolveNavHref } from "./nav-href";
import { categoryHref, isNavActive } from "./nav-utils";
function isDownloadAppNav(name?: string | null) {
return (name || "").trim() === "下载APP";
}
function withDividers(items: ReactNode[], divider: string) {
return items.flatMap((item, index) =>
index === 0 ? [item] : [<span key={`divider-${index}`} className={`shrink-0 ${divider}`} />, item],
);
}
/** 联系方式只保留官方 TG */
const contacts = [
{
key: "tg",
node: (
<a href={OFFICIAL_TG_URL} target="_blank" rel="noopener noreferrer" className="text-xs leading-[18px] text-[#615970]">
官方TG:{OFFICIAL_TG_HANDLE}
</a>
),
},
];
const socials = [{ label: "Telegram", icon: "/images/footer_tg.svg", href: OFFICIAL_TG_URL }];
function InfoNavLink({ item, onClose }: { item: TranslationInfo; onClose: () => void }) {
const href = resolveAboutPlatformHref(item);
const external = /^https?:\/\//i.test(href);
const className = "text-xs leading-[18px] text-[#948AA3]/90";
if (external) {
return (
<a href={href} target="_blank" rel="noopener noreferrer" onClick={onClose} className={className}>
{item.name}
</a>
);
}
return (
<Link href={href} onClick={onClose} className={className}>
{item.name}
</Link>
);
}
export function MobileMenu({
open,
onClose,
categoryList,
bottonTranslations,
}: {
open: boolean;
onClose: () => void;
categoryList?: TranslationCategory[] | null;
bottonTranslations?: AppTranslationInfo | null;
config?: ConfigInfo | null;
}) {
const pathname = usePathname();
const categories = categoryList ?? [];
const [expandedId, setExpandedId] = useState<number | null>(null);
// 仅渲染接口 bottonTranslations,不补自定义项
const bottomNavLinks = footerNavItems(bottonTranslations);
const downloadHref = useAppDownloadHref();
useEffect(() => {
if (!open) return;
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") onClose();
};
const unlockScroll = lockBodyScroll();
window.addEventListener("keydown", onKeyDown);
return () => {
unlockScroll();
window.removeEventListener("keydown", onKeyDown);
};
}, [open, onClose]);
return (
<div
data-component="mobile-menu"
className={`fixed inset-0 z-50 lg:hidden ${open ? "" : "pointer-events-none"}`}
{...(open ? {} : { inert: true as const })}
>
<div
className={`absolute inset-0 bg-black/60 transition-opacity duration-300 ${open ? "opacity-100" : "opacity-0"}`}
onClick={onClose}
/>
<div
role="dialog"
aria-modal="true"
aria-label="菜单"
className={`absolute inset-y-0 left-0 flex w-[281px] max-w-[85vw] flex-col items-center gap-6 overflow-y-auto [scrollbar-width:none] bg-[#0B0914] px-9 py-[60px] transition-transform duration-300 ${
open ? "translate-x-0" : "-translate-x-full"
}`}
>
<button type="button" aria-label="关闭菜单" onClick={onClose} className="absolute top-[26px] right-[13px] size-6 cursor-pointer">
<svg viewBox="0 0 24 24" fill="none" className="size-full" aria-hidden="true">
<path d="M6 6l12 12M18 6L6 18" stroke="white" strokeWidth="2" strokeLinecap="round" />
</svg>
</button>
<Link href="/" onClick={onClose} aria-label="黄果黑料" className="shrink-0">
<img src="/logo.svg" alt="黄果黑料" className="h-[62px] w-[137px]" />
</Link>
<nav className="flex w-full flex-col gap-[11px]" aria-label="主导航">
{categories.map((category) => {
const href = categoryHref(category);
const active = isNavActive(pathname, href);
const children = category.translationList?.filter((item) => item.name) ?? [];
const hasChildren = children.length > 0;
const expanded = expandedId === category.id;
if (hasChildren) {
return (
<div key={category.id} className="flex flex-col gap-1.5">
<button
type="button"
aria-expanded={expanded}
onClick={() => setExpandedId(expanded ? null : category.id)}
className={`flex h-11 shrink-0 items-center justify-between rounded-[9px] bg-white/10 px-[18px] text-base leading-[22px] font-medium ${
active ? "text-[#F12B9E]" : "text-white/90"
}`}
>
<span>{category.name}</span>
<span className={`text-xs transition-transform ${expanded ? "rotate-180" : ""}`}>▾</span>
</button>
{expanded ? (
<div className="flex flex-col gap-1 pl-2">
{children.map((item) => {
const childHref = resolveNavHref(item.urlValue);
const childActive = isNavActive(pathname, childHref);
return (
<Link
key={item.id}
href={childHref}
onClick={onClose}
className={`flex h-9 items-center rounded-lg px-4 text-sm ${
childActive ? "text-[#F12B9E]" : "text-white/70"
}`}
>
{item.name}
</Link>
);
})}
</div>
) : null}
</div>
);
}
if (isDownloadAppNav(category.name)) {
const className =
"flex h-11 w-full shrink-0 cursor-pointer items-center rounded-[9px] bg-white/10 px-[18px] text-left text-base leading-[22px] font-medium text-white/90";
if (downloadHref) {
return (
<a
key={category.id}
href={downloadHref}
target="_blank"
rel="noopener noreferrer"
onClick={onClose}
className={className}
>
{category.name}
</a>
);
}
return (
<button
key={category.id}
type="button"
onClick={() => {
onClose();
openAppDownload();
}}
className={className}
>
{category.name}
</button>
);
}
return (
<Link
key={category.id}
href={href}
onClick={onClose}
aria-current={active ? "page" : undefined}
className={`flex h-11 shrink-0 items-center rounded-[9px] bg-white/10 px-[18px] text-base leading-[22px] font-medium ${
active ? "text-[#F12B9E]" : "text-white/90"
}`}
>
{category.name}
</Link>
);
})}
</nav>
{bottomNavLinks.length ? (
<nav
aria-label="底部导航"
className="flex w-full flex-wrap items-center justify-center gap-1.5 py-0.5"
>
{withDividers(
bottomNavLinks.map((link) => (
<InfoNavLink key={link.id || link.name} item={link} onClose={onClose} />
)),
"h-5 w-px bg-white/20",
)}
</nav>
) : null}
{contacts.length ? (
<div className="flex w-full flex-wrap items-center justify-center gap-3">
{withDividers(
contacts.map((item) => <span key={item.key}>{item.node}</span>),
"h-[18px] w-px bg-[#615970]",
)}
</div>
) : null}
{socials.length ? (
<div className="flex items-center gap-6">
{withDividers(
socials.map((social) => (
<a
key={social.label}
href={social.href}
target={social.href.startsWith("http") ? "_blank" : undefined}
rel={social.href.startsWith("http") ? "noopener noreferrer" : undefined}
aria-label={social.label}
className="size-6"
>
<img src={social.icon} alt="" className="size-full" />
</a>
)),
"h-[15px] w-px bg-white/20",
)}
</div>
) : null}
</div>
</div>
);
}