Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
163 lines
5.5 KiB
TypeScript
163 lines
5.5 KiB
TypeScript
'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
|