admin/components/layouts/SidebarMobile.tsx
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

163 lines
5.5 KiB
TypeScript
Raw Permalink 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 React, { Suspense, useCallback, useEffect, useMemo, useRef } from 'react'
import { usePathname } from 'next/navigation'
import Image from 'next/image'
import '@/styles/userSidebar.scss'
import { siteConfig } from '@/config/site'
import Loading from '@/components/layouts/Loading'
import SidebarUserFooter from '@/components/layouts/SidebarUserFooter'
import SidebarNav from '@/components/layouts/sidebar/SidebarNav'
import Button from '@/components/formElements/Button'
import { MOBILE_SIDEBAR_WIDTH } from '@/config/sidebar'
import MenuBurgerIcon from '@/components/icons/MenuBurgerIcon'
import CloseIcon from '@/components/icons/CloseIcon'
import { withBasePath } from '@/constants/images'
const FOCUSABLE_SELECTOR =
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
const SidebarMobile = () => {
const [isOpen, setIsOpen] = React.useState(false)
const triggerRef = useRef<HTMLButtonElement>(null)
const closeButtonRef = useRef<HTMLButtonElement>(null)
const panelRef = useRef<HTMLDivElement>(null)
const pathname = usePathname()
const accessibleMenus = useMemo(() => siteConfig().userSidebar, [])
const handleLinkClick = () => {
setIsOpen(false)
}
const closeMenu = useCallback(() => {
setIsOpen(false)
window.requestAnimationFrame(() => triggerRef.current?.focus())
}, [])
useEffect(() => {
if (!isOpen) return
const previousOverflow = document.body.style.overflow
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
closeMenu()
return
}
if (event.key !== 'Tab') return
const focusableElements = Array.from(panelRef.current?.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR) ?? [])
const firstElement = focusableElements[0]
const lastElement = focusableElements.at(-1)
if (!firstElement || !lastElement) return
if (event.shiftKey && document.activeElement === firstElement) {
event.preventDefault()
lastElement.focus()
} else if (!event.shiftKey && document.activeElement === lastElement) {
event.preventDefault()
firstElement.focus()
}
}
document.body.style.overflow = 'hidden'
document.addEventListener('keydown', handleKeyDown)
window.requestAnimationFrame(() => closeButtonRef.current?.focus())
return () => {
document.body.style.overflow = previousOverflow
document.removeEventListener('keydown', handleKeyDown)
}
}, [closeMenu, isOpen])
return (
<>
<Button
ref={triggerRef}
iconOnly
aria-controls="admin-mobile-navigation"
aria-expanded={isOpen}
aria-label="باز کردن منو"
className="ms-2 rounded-xl border border-secondary-40 bg-white p-2 shadow-sm hover:bg-secondary-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
variant="bordered"
onClick={() => {
setIsOpen(true)
}}
>
<MenuBurgerIcon className="size-6 text-tertiary-600" />
</Button>
{isOpen ? (
<div
aria-hidden="true"
className="fixed inset-0 z-40 bg-black/50 md:hidden"
onClick={closeMenu}
/>
) : null}
<div
ref={panelRef}
aria-hidden={!isOpen}
aria-label="منوی مدیریت"
aria-modal="true"
className={`sidebar fixed start-0 top-0 z-50 flex h-[100dvh] flex-col border-e border-secondary-40 bg-white shadow-2xl transition-transform duration-300 ease-in-out motion-reduce:transition-none md:hidden ${
isOpen ? 'translate-x-0' : 'translate-x-full'
}`}
id="admin-mobile-navigation"
inert={!isOpen}
role="dialog"
style={{ width: MOBILE_SIDEBAR_WIDTH }}
>
<div className="relative mx-4 flex h-24 shrink-0 items-center gap-3 border-b border-secondary-40 px-2">
<Button
ref={closeButtonRef}
iconOnly
aria-label="بستن منو"
className="absolute end-1 top-1 rounded-lg p-2 hover:bg-primary-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
size="sm"
variant="light"
onClick={closeMenu}
>
<CloseIcon className="size-5 text-tertiary-600" />
</Button>
<div className="flex size-11 items-center justify-center rounded-2xl bg-primary-600">
<Image
alt="قبیله"
className="size-7 brightness-0 invert"
height={28}
src={withBasePath('/logo.svg')}
width={28}
/>
</div>
<div className="text-right">
<h2 className="font-bold text-secondary-10">مدیریت قبیله</h2>
<p className="text-smt-neutral-30">پنل مدیریت پلتفرم</p>
</div>
</div>
<div className="pb-3 flex flex-col justify-between flex-1 min-h-0 overflow-auto">
<div className="px-3 pt-4">
<p className="mb-2 px-3 text-[10px] font-bold text-secondary-30">منوی مدیریت</p>
<SidebarNav
items={accessibleMenus}
pathname={pathname}
onLinkClick={handleLinkClick}
/>
</div>
<SidebarUserFooter />
</div>
</div>
</>
)
}
const SidebarMobileWrapper = () => {
return (
<Suspense fallback={<Loading />}>
<SidebarMobile />
</Suspense>
)
}
export default SidebarMobileWrapper