Audit fixes: VIP self-grant, draft leak, onboarding flag, hero personalization, gated downloads
Deploy GitHub Pages / deploy (push) Canceled after 0s

This commit is contained in:
AHMED
2026-10-09 16:20:55 +03:00
parent c34611dc36
commit 5ee706ebd6
150 changed files with 24054 additions and 0 deletions
+94
View File
@@ -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>
)
}