mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2025-12-13 07:46:10 +00:00
* types: define types and remove file-wide eslint disables * reduce coverage requirement
180 lines
6.8 KiB
TypeScript
180 lines
6.8 KiB
TypeScript
import ChevronRightIcon from '@heroicons/react/24/outline/ChevronRightIcon'
|
|
import { useRef, useState, useEffect } from 'react'
|
|
import { NavLink, useLocation } from 'react-router-dom'
|
|
import { Sidenav, initTE } from 'tw-elements'
|
|
|
|
import SidebarSubmenu from './SidebarSubmenu'
|
|
|
|
export interface Route {
|
|
path: string
|
|
icon: JSX.Element
|
|
name: string
|
|
submenu?: Route[]
|
|
blank?: boolean
|
|
}
|
|
|
|
interface SidenavType extends HTMLElement {
|
|
toggleSlim(): void
|
|
toggle(): void
|
|
}
|
|
|
|
/**
|
|
* @category AppShell
|
|
*/
|
|
export function SideBar({ routes, bottomRoutes }: { routes: Route[]; bottomRoutes?: Route[] }) {
|
|
// prevent react18 from calling useEffect twice
|
|
const init = useRef(false)
|
|
|
|
const location = useLocation()
|
|
|
|
const [instance, setInstance] = useState<SidenavType>()
|
|
const [slim, setSlim] = useState<boolean>(false)
|
|
|
|
const toggleSlim = () => {
|
|
setSlim(!slim)
|
|
instance?.toggleSlim()
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (!init.current) {
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
|
|
initTE({ Sidenav })
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
|
const instance = Sidenav.getInstance(document.getElementById('sidenav')) as SidenavType
|
|
setInstance(instance)
|
|
instance.toggleSlim()
|
|
init.current = true
|
|
}
|
|
}, [])
|
|
|
|
const [embedded, setEmbedded] = useState<boolean>(true)
|
|
|
|
useEffect(() => {
|
|
const params = new URLSearchParams(location.search)
|
|
const embedded = params.get('embedded')
|
|
embedded !== 'true' && setEmbedded(false)
|
|
}, [location])
|
|
|
|
const params = new URLSearchParams(window.location.search)
|
|
|
|
return (
|
|
<nav
|
|
id='sidenav'
|
|
className={`group tw-fixed tw-left-0 ${embedded ? 'tw-mt-0 tw-h-[100dvh]' : 'tw-mt-16 tw-h-[calc(100dvh-64px)]'} tw-top-0 tw-z-[10035] tw--translate-x-full tw-overflow-hidden tw-shadow-xl data-[te-sidenav-slim='true']:tw-hidden data-[te-sidenav-slim-collapsed='true']:tw-w-[56px] data-[te-sidenav-slim='true']:tw-w-[56px] data-[te-sidenav-hidden='false']:tw-translate-x-0 dark:tw-bg-zinc-800 [&[data-te-sidenav-slim-collapsed='true'][data-te-sidenav-slim='false']]:tw-hidden [&[data-te-sidenav-slim-collapsed='true'][data-te-sidenav-slim='true']]:[display:unset]`}
|
|
data-te-sidenav-init
|
|
data-te-sidenav-hidden='true'
|
|
data-te-sidenav-mode='side'
|
|
data-te-sidenav-slim='true'
|
|
data-te-sidenav-content='#app-content'
|
|
data-te-sidenav-slim-collapsed='true'
|
|
data-te-sidenav-slim-width='56'
|
|
data-te-sidenav-width='180'
|
|
>
|
|
<div
|
|
className={`tw-flex tw-flex-col ${embedded ? 'tw-h-full' : 'tw-h-[calc(100dvh-64px)]'}`}
|
|
>
|
|
<ul
|
|
className='tw-menu tw-w-full tw-bg-base-100 tw-text-base-content tw-p-0'
|
|
data-te-sidenav-menu-ref
|
|
>
|
|
{routes.map((route, k) => {
|
|
return (
|
|
<li className='' key={k}>
|
|
{route.submenu ? (
|
|
<SidebarSubmenu {...route} />
|
|
) : (
|
|
<NavLink
|
|
end
|
|
target={route.blank ? '_blank' : '_self'}
|
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
to={`${route.path}${params && '?' + params.toString()}`}
|
|
className={({ isActive }) =>
|
|
`${isActive ? 'tw-font-semibold tw-bg-base-200 !tw-rounded-none' : 'tw-font-normal !tw-rounded-none'}`
|
|
}
|
|
onClick={() => {
|
|
if (screen.width < 640 && !slim) instance?.toggle()
|
|
}}
|
|
>
|
|
{route.icon}
|
|
<span
|
|
className="group-[&[data-te-sidenav-slim-collapsed='true']]:data-[te-sidenav-slim='false']:tw-hidden"
|
|
data-te-sidenav-slim='false'
|
|
>
|
|
{route.name}
|
|
</span>
|
|
{(location.pathname.includes(route.path) && route.path.length > 1) ||
|
|
location.pathname === route.path ? (
|
|
<span
|
|
className='tw-absolute tw-inset-y-0 tw-left-0 tw-w-1 tw-rounded-tr-md tw-rounded-br-md tw-bg-primary '
|
|
aria-hidden='true'
|
|
></span>
|
|
) : null}
|
|
</NavLink>
|
|
)}
|
|
</li>
|
|
)
|
|
})}
|
|
</ul>
|
|
|
|
<div
|
|
id='slim-toggler'
|
|
className='tw-w-full tw-bg-base-100 tw-flex-1 tw-grid tw-place-items-end'
|
|
aria-haspopup='true'
|
|
>
|
|
<div className='tw-w-full'>
|
|
<ul
|
|
className='tw-menu tw-w-full tw-bg-base-100 tw-text-base-content tw-p-0 tw-mb-0'
|
|
data-te-sidenav-menu-ref
|
|
>
|
|
{bottomRoutes?.map((route, k) => {
|
|
return (
|
|
<li className='' key={k}>
|
|
{route.submenu ? (
|
|
<SidebarSubmenu {...route} />
|
|
) : (
|
|
<NavLink
|
|
end
|
|
target={route.blank ? '_blank' : '_self'}
|
|
to={route.path}
|
|
className={({ isActive }) =>
|
|
`${isActive ? 'tw-font-semibold tw-bg-base-200 !tw-rounded-none' : 'tw-font-normal !tw-rounded-none'}`
|
|
}
|
|
onClick={() => {
|
|
if (screen.width < 640 && !slim) instance?.toggle()
|
|
}}
|
|
>
|
|
{route.icon}
|
|
<span
|
|
className="group-[&[data-te-sidenav-slim-collapsed='true']]:data-[te-sidenav-slim='false']:tw-hidden"
|
|
data-te-sidenav-slim='false'
|
|
>
|
|
{route.name}
|
|
</span>
|
|
{(location.pathname.includes(route.path) && route.path.length > 1) ||
|
|
location.pathname === route.path ? (
|
|
<span
|
|
className='tw-absolute tw-inset-y-0 tw-left-0 tw-w-1 tw-rounded-tr-md tw-rounded-br-md tw-bg-primary '
|
|
aria-hidden='true'
|
|
></span>
|
|
) : null}
|
|
</NavLink>
|
|
)}
|
|
</li>
|
|
)
|
|
})}
|
|
</ul>
|
|
|
|
<ChevronRightIcon
|
|
className={
|
|
'tw-w-5 tw-h-5 tw-mb-4 tw-mr-4 tw-cursor-pointer tw-float-right tw-delay-400 tw-duration-500 tw-transition-all ' +
|
|
(!slim ? 'tw-rotate-180' : '')
|
|
}
|
|
onClick={() => toggleSlim()}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
)
|
|
}
|