Audit fixes: VIP self-grant, draft leak, onboarding flag, hero personalization, gated downloads
Deploy GitHub Pages / deploy (push) Canceled after 0s
Deploy GitHub Pages / deploy (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { SITE_LINKS } from './site-links'
|
||||
import AccountLink from './account-link'
|
||||
|
||||
/* Shared mobile navigation, including keyboard focus and background locking. */
|
||||
export default function SiteMenu({
|
||||
menuId = 'siteMenu',
|
||||
viewer,
|
||||
}: {
|
||||
menuId?: string
|
||||
viewer?: { email: string } | null
|
||||
}) {
|
||||
const pathname = usePathname()
|
||||
|
||||
useEffect(() => {
|
||||
const menu = document.getElementById(menuId)
|
||||
const trigger = document.getElementById('menuTrigger')
|
||||
if (!menu || !trigger) return
|
||||
const background = [...document.querySelectorAll<HTMLElement>('#site > *')]
|
||||
.filter(element => element !== menu)
|
||||
const originalInert = background.map(element => element.inert)
|
||||
|
||||
function setOpen(open: boolean) {
|
||||
menu!.classList.toggle('open', open)
|
||||
menu!.setAttribute('aria-hidden', String(!open))
|
||||
trigger!.setAttribute('aria-expanded', String(open))
|
||||
document.body.classList.toggle('locked', open)
|
||||
background.forEach((element, index) => { element.inert = open || originalInert[index] })
|
||||
if (open) (menu!.querySelector('.menu-head [data-menu-close]') as HTMLElement | null)?.focus()
|
||||
else trigger!.focus()
|
||||
}
|
||||
|
||||
const onTrigger = () => setOpen(true)
|
||||
const onScrim = (e: Event) => {
|
||||
if ((e.target as HTMLElement).closest('[data-menu-close]')) setOpen(false)
|
||||
}
|
||||
const onLink = () => setOpen(false)
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && menu!.classList.contains('open')) setOpen(false)
|
||||
if (e.key === 'Tab' && menu!.classList.contains('open')) {
|
||||
const focusable = [...menu!.querySelectorAll<HTMLElement>('a[href], button:not([disabled])')]
|
||||
.filter(element => element.getClientRects().length && !element.classList.contains('menu-scrim'))
|
||||
const first = focusable[0]
|
||||
const last = focusable[focusable.length - 1]
|
||||
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last?.focus() }
|
||||
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first?.focus() }
|
||||
}
|
||||
}
|
||||
|
||||
trigger.addEventListener('click', onTrigger)
|
||||
menu.addEventListener('click', onScrim)
|
||||
menu.querySelectorAll('a').forEach((a) => a.addEventListener('click', onLink))
|
||||
document.addEventListener('keydown', onKey)
|
||||
return () => {
|
||||
trigger.removeEventListener('click', onTrigger)
|
||||
menu.removeEventListener('click', onScrim)
|
||||
menu.querySelectorAll('a').forEach(a => a.removeEventListener('click', onLink))
|
||||
document.removeEventListener('keydown', onKey)
|
||||
document.body.classList.remove('locked')
|
||||
background.forEach((element, index) => { element.inert = originalInert[index] })
|
||||
}
|
||||
}, [menuId])
|
||||
|
||||
return (
|
||||
<aside className="menu-panel" id={menuId} aria-hidden="true" data-od-id="site-menu">
|
||||
<button className="menu-scrim" type="button" aria-label="إغلاق القائمة" data-menu-close></button>
|
||||
<div className="menu-drawer" role="dialog" aria-modal="true" aria-label="قائمة أنور بآي">
|
||||
<div className="menu-head">
|
||||
<Link className="logo" href="/">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img className="logo-mark" src="/brand/mark-light.svg" alt="" width={36} height={32} />
|
||||
أنور بآي
|
||||
</Link>
|
||||
<button className="icon-btn" type="button" aria-label="إغلاق القائمة" data-menu-close>×</button>
|
||||
</div>
|
||||
<nav className="menu-links" aria-label="قائمة الموقع">
|
||||
{SITE_LINKS.map((l) => (
|
||||
<Link key={l.href} href={l.href} aria-current={pathname === l.href ? 'page' : undefined}>
|
||||
{l.label} <span>←</span>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
<div className="menu-foot">
|
||||
<AccountLink viewer={viewer} />
|
||||
<p>محتوى عربي عملي، بلا ضجيج.</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user