feat: new mobile menu (#3251)
parent
19370f856c
commit
fcbca1722f
@ -0,0 +1,206 @@
|
||||
import { menuMessages } from '@app/components/Layout/Sidebar';
|
||||
import useClickOutside from '@app/hooks/useClickOutside';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import {
|
||||
ClockIcon,
|
||||
CogIcon,
|
||||
EllipsisHorizontalIcon,
|
||||
ExclamationTriangleIcon,
|
||||
FilmIcon,
|
||||
SparklesIcon,
|
||||
TvIcon,
|
||||
UsersIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import {
|
||||
ClockIcon as FilledClockIcon,
|
||||
CogIcon as FilledCogIcon,
|
||||
ExclamationTriangleIcon as FilledExclamationTriangleIcon,
|
||||
FilmIcon as FilledFilmIcon,
|
||||
SparklesIcon as FilledSparklesIcon,
|
||||
TvIcon as FilledTvIcon,
|
||||
UsersIcon as FilledUsersIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/react/24/solid';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { cloneElement, useRef, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
interface MenuLink {
|
||||
href: string;
|
||||
svgIcon: JSX.Element;
|
||||
svgIconSelected: JSX.Element;
|
||||
content: React.ReactNode;
|
||||
activeRegExp: RegExp;
|
||||
as?: string;
|
||||
requiredPermission?: Permission | Permission[];
|
||||
permissionType?: 'and' | 'or';
|
||||
dataTestId?: string;
|
||||
}
|
||||
|
||||
const MobileMenu = () => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const intl = useIntl();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { hasPermission } = useUser();
|
||||
const router = useRouter();
|
||||
useClickOutside(ref, () => {
|
||||
setTimeout(() => {
|
||||
if (isOpen) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}, 150);
|
||||
});
|
||||
|
||||
const toggle = () => setIsOpen(!isOpen);
|
||||
|
||||
const menuLinks: MenuLink[] = [
|
||||
{
|
||||
href: '/',
|
||||
content: intl.formatMessage(menuMessages.dashboard),
|
||||
svgIcon: <SparklesIcon className="h-6 w-6" />,
|
||||
svgIconSelected: <FilledSparklesIcon className="h-6 w-6" />,
|
||||
activeRegExp: /^\/(discover\/?)?$/,
|
||||
},
|
||||
{
|
||||
href: '/discover/movies',
|
||||
content: intl.formatMessage(menuMessages.browsemovies),
|
||||
svgIcon: <FilmIcon className="h-6 w-6" />,
|
||||
svgIconSelected: <FilledFilmIcon className="h-6 w-6" />,
|
||||
activeRegExp: /^\/discover\/movies$/,
|
||||
},
|
||||
{
|
||||
href: '/discover/tv',
|
||||
content: intl.formatMessage(menuMessages.browsetv),
|
||||
svgIcon: <TvIcon className="h-6 w-6" />,
|
||||
svgIconSelected: <FilledTvIcon className="h-6 w-6" />,
|
||||
activeRegExp: /^\/discover\/tv$/,
|
||||
},
|
||||
{
|
||||
href: '/requests',
|
||||
content: intl.formatMessage(menuMessages.requests),
|
||||
svgIcon: <ClockIcon className="h-6 w-6" />,
|
||||
svgIconSelected: <FilledClockIcon className="h-6 w-6" />,
|
||||
activeRegExp: /^\/requests/,
|
||||
},
|
||||
{
|
||||
href: '/issues',
|
||||
content: intl.formatMessage(menuMessages.issues),
|
||||
svgIcon: <ExclamationTriangleIcon className="h-6 w-6" />,
|
||||
svgIconSelected: <FilledExclamationTriangleIcon className="h-6 w-6" />,
|
||||
activeRegExp: /^\/issues/,
|
||||
requiredPermission: [
|
||||
Permission.MANAGE_ISSUES,
|
||||
Permission.CREATE_ISSUES,
|
||||
Permission.VIEW_ISSUES,
|
||||
],
|
||||
permissionType: 'or',
|
||||
},
|
||||
{
|
||||
href: '/users',
|
||||
content: intl.formatMessage(menuMessages.users),
|
||||
svgIcon: <UsersIcon className="mr-3 h-6 w-6" />,
|
||||
svgIconSelected: <FilledUsersIcon className="mr-3 h-6 w-6" />,
|
||||
activeRegExp: /^\/users/,
|
||||
requiredPermission: Permission.MANAGE_USERS,
|
||||
dataTestId: 'sidebar-menu-users',
|
||||
},
|
||||
{
|
||||
href: '/settings',
|
||||
content: intl.formatMessage(menuMessages.settings),
|
||||
svgIcon: <CogIcon className="mr-3 h-6 w-6" />,
|
||||
svgIconSelected: <FilledCogIcon className="mr-3 h-6 w-6" />,
|
||||
activeRegExp: /^\/settings/,
|
||||
requiredPermission: Permission.ADMIN,
|
||||
dataTestId: 'sidebar-menu-settings',
|
||||
},
|
||||
];
|
||||
|
||||
const filteredLinks = menuLinks.filter(
|
||||
(link) => !link.requiredPermission || hasPermission(link.requiredPermission)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-0 left-0 right-0 z-50">
|
||||
<Transition
|
||||
show={isOpen}
|
||||
as="div"
|
||||
ref={ref}
|
||||
enter="transition transform duration-500"
|
||||
enterFrom="opacity-0 translate-y-0"
|
||||
enterTo="opacity-100 -translate-y-full"
|
||||
leave="transition duration-500 transform"
|
||||
leaveFrom="opacity-100 -translate-y-full"
|
||||
leaveTo="opacity-0 translate-y-0"
|
||||
className="absolute top-0 left-0 right-0 flex w-full -translate-y-full transform flex-col space-y-6 border-t border-gray-600 bg-gray-900 bg-opacity-90 px-6 py-6 font-semibold text-gray-100 backdrop-blur"
|
||||
>
|
||||
{filteredLinks.map((link) => {
|
||||
const isActive = router.pathname.match(link.activeRegExp);
|
||||
return (
|
||||
<Link key={`mobile-menu-link-${link.href}`} href={link.href}>
|
||||
<a
|
||||
className={`flex items-center space-x-2 ${
|
||||
isActive ? 'text-indigo-500' : ''
|
||||
}`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}}
|
||||
onClick={() => setIsOpen(false)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
{cloneElement(isActive ? link.svgIconSelected : link.svgIcon, {
|
||||
className: 'h-5 w-5',
|
||||
})}
|
||||
<span>{link.content}</span>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</Transition>
|
||||
<div className="padding-bottom-safe border-t border-gray-600 bg-gray-800 bg-opacity-90 backdrop-blur">
|
||||
<div className="flex h-full items-center justify-between px-6 py-4 text-gray-100">
|
||||
{filteredLinks.slice(0, 4).map((link) => {
|
||||
const isActive =
|
||||
router.pathname.match(link.activeRegExp) && !isOpen;
|
||||
return (
|
||||
<Link key={`mobile-menu-link-${link.href}`} href={link.href}>
|
||||
<a
|
||||
className={`flex flex-col items-center space-y-1 ${
|
||||
isActive ? 'text-indigo-500' : ''
|
||||
}`}
|
||||
>
|
||||
{cloneElement(
|
||||
isActive ? link.svgIconSelected : link.svgIcon,
|
||||
{
|
||||
className: 'h-6 w-6',
|
||||
}
|
||||
)}
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
{filteredLinks.length > 4 && (
|
||||
<button
|
||||
className={`flex flex-col items-center space-y-1 ${
|
||||
isOpen ? 'text-indigo-500' : ''
|
||||
}`}
|
||||
onClick={() => toggle()}
|
||||
>
|
||||
{isOpen ? (
|
||||
<XMarkIcon className="h-6 w-6" />
|
||||
) : (
|
||||
<EllipsisHorizontalIcon className="h-6 w-6" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileMenu;
|
Loading…
Reference in new issue