Create multi-item admin menu
This commit is contained in:
parent
bc24d42864
commit
9f483bcf21
@ -4,15 +4,27 @@ import MoreMenu from '@/components/more/MoreMenu';
|
||||
import {
|
||||
PATH_ADMIN_CONFIGURATION,
|
||||
PATH_ADMIN_INSIGHTS,
|
||||
PATH_ADMIN_PHOTOS,
|
||||
PATH_ADMIN_TAGS,
|
||||
PATH_GRID_INFERRED,
|
||||
} from '@/app/paths';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import { ImCheckboxUnchecked } from 'react-icons/im';
|
||||
import { IoCloseSharp } from 'react-icons/io5';
|
||||
import AdminAppInsightsIcon from './insights/AdminAppInsightsIcon';
|
||||
import { LuCog } from 'react-icons/lu';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { TbPhoto } from 'react-icons/tb';
|
||||
import { FiTag } from 'react-icons/fi';
|
||||
import { BiLockAlt } from 'react-icons/bi';
|
||||
import AdminAppInsightsIcon from './insights/AdminAppInsightsIcon';
|
||||
|
||||
export default function AdminAppMenu() {
|
||||
export default function AdminAppMenu({
|
||||
className,
|
||||
buttonClassName,
|
||||
}: {
|
||||
className?: string
|
||||
buttonClassName?: string
|
||||
}) {
|
||||
const {
|
||||
selectedPhotoIds,
|
||||
setSelectedPhotoIds,
|
||||
@ -22,28 +34,51 @@ export default function AdminAppMenu() {
|
||||
|
||||
return (
|
||||
<MoreMenu
|
||||
header="Admin menu"
|
||||
icon={<BiLockAlt size={16} className="translate-y-[-0.5px]" />}
|
||||
align="start"
|
||||
className={clsx(
|
||||
'border-medium',
|
||||
className,
|
||||
)}
|
||||
buttonClassName={clsx(
|
||||
'rounded-none focus:outline-none',
|
||||
buttonClassName,
|
||||
)}
|
||||
items={[{
|
||||
label: 'Manage Photos',
|
||||
icon: <TbPhoto
|
||||
size={16}
|
||||
className="translate-x-[1px] translate-y-[0.5px]"
|
||||
/>,
|
||||
href: PATH_ADMIN_PHOTOS,
|
||||
}, {
|
||||
label: 'Manage Tags',
|
||||
icon: <FiTag
|
||||
size={16}
|
||||
className="translate-x-[1.5px] translate-y-[0.5px]"
|
||||
/>,
|
||||
href: PATH_ADMIN_TAGS,
|
||||
}, {
|
||||
label: 'Insights',
|
||||
icon: <span className="scale-90 translate-y-[-2px]">
|
||||
<AdminAppInsightsIcon />
|
||||
</span>,
|
||||
icon: <AdminAppInsightsIcon className="translate-y-[-4px]" />,
|
||||
href: PATH_ADMIN_INSIGHTS,
|
||||
}, {
|
||||
label: 'Configuration',
|
||||
icon: <LuCog
|
||||
className="text-[16px] translate-x-[0.5px]"
|
||||
className="text-[17px] translate-x-[0.5px] translate-y-[0.5px]"
|
||||
/>,
|
||||
href: PATH_ADMIN_CONFIGURATION,
|
||||
}, {
|
||||
label: isSelecting
|
||||
? 'Exit Select'
|
||||
: 'Select',
|
||||
: 'Select Photos',
|
||||
icon: isSelecting
|
||||
? <IoCloseSharp
|
||||
className="text-[18px] translate-y-[-0.5px]"
|
||||
/>
|
||||
: <ImCheckboxUnchecked
|
||||
className="text-[0.75rem] translate-x-[0.5px]"
|
||||
className="text-[0.75rem] translate-x-[1px]"
|
||||
/>,
|
||||
href: PATH_GRID_INFERRED,
|
||||
action: () => {
|
||||
|
||||
@ -47,7 +47,10 @@ export default function AdminPhotoMenuClient({
|
||||
const items = useMemo(() => {
|
||||
const items: ComponentProps<typeof MoreMenuItem>[] = [{
|
||||
label: 'Edit',
|
||||
icon: <FaRegEdit size={14} />,
|
||||
icon: <FaRegEdit
|
||||
size={15}
|
||||
className="translate-x-[0.5px] translate-y-[-0.5px]"
|
||||
/>,
|
||||
href: pathForAdminPhotoEdit(photo.id),
|
||||
}];
|
||||
if (includeFavorite) {
|
||||
|
||||
@ -2,13 +2,20 @@ import { useAppState } from '@/state/AppState';
|
||||
import clsx from 'clsx/lite';
|
||||
import { LuLightbulb } from 'react-icons/lu';
|
||||
import { FaCircle } from 'react-icons/fa6';
|
||||
export default function AdminAppInsightsIcon() {
|
||||
export default function AdminAppInsightsIcon({
|
||||
className,
|
||||
}: {
|
||||
className?: string
|
||||
}) {
|
||||
const {
|
||||
insightIndicatorStatus,
|
||||
} = useAppState();
|
||||
|
||||
return (
|
||||
<span className="inline-flex relative">
|
||||
<span className={clsx(
|
||||
'inline-flex relative',
|
||||
className,
|
||||
)}>
|
||||
<LuLightbulb
|
||||
size={18}
|
||||
className="translate-y-[3.5px]"
|
||||
|
||||
@ -14,13 +14,11 @@ import {
|
||||
isPathSignIn,
|
||||
} from '@/app/paths';
|
||||
import AnimateItems from '../components/AnimateItems';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import {
|
||||
GRID_HOMEPAGE_ENABLED,
|
||||
HAS_DEFINED_SITE_DESCRIPTION,
|
||||
SITE_DESCRIPTION,
|
||||
} from './config';
|
||||
import AdminAppMenu from '@/admin/AdminAppMenu';
|
||||
|
||||
const NAV_HEIGHT_CLASS = HAS_DEFINED_SITE_DESCRIPTION
|
||||
? 'min-h-[4rem] sm:min-h-[5rem]'
|
||||
@ -33,8 +31,6 @@ export default function Nav({
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
|
||||
const { isUserSignedIn } = useAppState();
|
||||
|
||||
const showNav = !isPathSignIn(pathname);
|
||||
|
||||
const renderLink = (
|
||||
@ -73,7 +69,6 @@ export default function Nav({
|
||||
)}>
|
||||
<ViewSwitcher
|
||||
currentSelection={switcherSelectionForPath()}
|
||||
showAdmin={isUserSignedIn}
|
||||
/>
|
||||
<div className={clsx(
|
||||
'grow text-right min-w-0',
|
||||
@ -98,16 +93,6 @@ export default function Nav({
|
||||
: []}
|
||||
/>
|
||||
}
|
||||
contentSide={isUserSignedIn && !isPathAdmin(pathname)
|
||||
? <div
|
||||
className={clsx(
|
||||
'flex items-center translate-x-[-6px] w-full',
|
||||
NAV_HEIGHT_CLASS,
|
||||
)}
|
||||
>
|
||||
<AdminAppMenu />
|
||||
</div>
|
||||
: undefined}
|
||||
sideHiddenOnMobile
|
||||
/>
|
||||
);
|
||||
|
||||
@ -3,25 +3,23 @@ import SwitcherItem from '@/components/SwitcherItem';
|
||||
import IconFeed from '@/app/IconFeed';
|
||||
import IconGrid from '@/app/IconGrid';
|
||||
import {
|
||||
PATH_ADMIN_PHOTOS,
|
||||
PATH_FEED_INFERRED,
|
||||
PATH_GRID_INFERRED,
|
||||
} from '@/app/paths';
|
||||
import { BiLockAlt } from 'react-icons/bi';
|
||||
import IconSearch from './IconSearch';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import { GRID_HOMEPAGE_ENABLED } from './config';
|
||||
import AdminAppMenu from '@/admin/AdminAppMenu';
|
||||
import { clsx } from 'clsx/lite';
|
||||
|
||||
export type SwitcherSelection = 'feed' | 'grid' | 'admin';
|
||||
|
||||
export default function ViewSwitcher({
|
||||
currentSelection,
|
||||
showAdmin,
|
||||
}: {
|
||||
currentSelection?: SwitcherSelection
|
||||
showAdmin?: boolean
|
||||
}) {
|
||||
const { setIsCommandKOpen } = useAppState();
|
||||
const { setIsCommandKOpen, isUserSignedIn } = useAppState();
|
||||
|
||||
const renderItemFeed = () =>
|
||||
<SwitcherItem
|
||||
@ -44,11 +42,17 @@ export default function ViewSwitcher({
|
||||
<Switcher>
|
||||
{GRID_HOMEPAGE_ENABLED ? renderItemGrid() : renderItemFeed()}
|
||||
{GRID_HOMEPAGE_ENABLED ? renderItemFeed() : renderItemGrid()}
|
||||
{showAdmin &&
|
||||
<SwitcherItem
|
||||
icon={<BiLockAlt size={16} className="translate-y-[-0.5px]" />}
|
||||
href={PATH_ADMIN_PHOTOS}
|
||||
active={currentSelection === 'admin'}
|
||||
{isUserSignedIn &&
|
||||
<AdminAppMenu
|
||||
className="mt-3 ml-[-84px]"
|
||||
buttonClassName={clsx(
|
||||
'w-[40px] h-[28px]',
|
||||
'flex items-center justify-center',
|
||||
'active:bg-transparent',
|
||||
currentSelection === 'admin'
|
||||
? 'text-black dark:text-white'
|
||||
: 'text-gray-400 dark:text-gray-600',
|
||||
)}
|
||||
/>}
|
||||
</Switcher>
|
||||
<Switcher type="borderless">
|
||||
|
||||
@ -11,10 +11,10 @@ export default function Switcher({
|
||||
return (
|
||||
<div className={clsx(
|
||||
'flex divide-x overflow-hidden',
|
||||
'divide-gray-300 dark:divide-gray-800',
|
||||
'divide-medium',
|
||||
'border rounded-md',
|
||||
type === 'regular'
|
||||
? 'border-gray-300 dark:border-gray-800'
|
||||
? 'border-medium'
|
||||
: 'border-transparent',
|
||||
type === 'regular' && 'shadow-xs',
|
||||
)}>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ComponentProps } from 'react';
|
||||
import { ComponentProps, ReactNode, useCallback, useState } from 'react';
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { FiMoreHorizontal } from 'react-icons/fi';
|
||||
@ -6,38 +6,50 @@ import MoreMenuItem from './MoreMenuItem';
|
||||
|
||||
export default function MoreMenu({
|
||||
items,
|
||||
icon,
|
||||
header,
|
||||
className,
|
||||
buttonClassName,
|
||||
ariaLabel,
|
||||
align = 'end',
|
||||
...props
|
||||
}: {
|
||||
items: ComponentProps<typeof MoreMenuItem> []
|
||||
icon?: ReactNode
|
||||
header?: ReactNode
|
||||
className?: string
|
||||
buttonClassName?: string
|
||||
ariaLabel: string
|
||||
}){
|
||||
} & ComponentProps<typeof DropdownMenu.Content>){
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const dismissMenu = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
}, [setIsOpen]);
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Root open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button
|
||||
className={clsx(
|
||||
buttonClassName,
|
||||
'p-1 min-h-0 border-none shadow-none hover:outline-hidden',
|
||||
'hover:bg-gray-100 active:bg-gray-100',
|
||||
'dark:hover:bg-gray-800/75 dark:active:bg-gray-900',
|
||||
'text-dim',
|
||||
buttonClassName,
|
||||
)}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<FiMoreHorizontal size={18} />
|
||||
{icon ?? <FiMoreHorizontal size={18} />}
|
||||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
align="end"
|
||||
{...props}
|
||||
align={align}
|
||||
className={clsx(
|
||||
'z-10',
|
||||
'min-w-[8rem]',
|
||||
'ml-2.5',
|
||||
'component-surface',
|
||||
'p-1',
|
||||
'shadow-lg dark:shadow-xl',
|
||||
@ -46,8 +58,18 @@ export default function MoreMenu({
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{header && <div className={clsx(
|
||||
'px-2 py-1.5 text-medium uppercase',
|
||||
'text-sm',
|
||||
)}>
|
||||
{header}
|
||||
</div>}
|
||||
{items.map(props =>
|
||||
<MoreMenuItem key={`${props.label}`} {...props} />,
|
||||
<MoreMenuItem
|
||||
key={`${props.label}`}
|
||||
{...props}
|
||||
dismissMenu={dismissMenu}
|
||||
/>,
|
||||
)}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { ReactNode, useState, useTransition } from 'react';
|
||||
import { ReactNode, useEffect, useState, useTransition } from 'react';
|
||||
import LoaderButton from '../primitives/LoaderButton';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { downloadFileFromBrowser } from '@/utility/url';
|
||||
@ -14,6 +14,7 @@ export default function MoreMenuItem({
|
||||
hrefDownloadName,
|
||||
className,
|
||||
action,
|
||||
dismissMenu,
|
||||
shouldPreventDefault = true,
|
||||
}: {
|
||||
label: ReactNode
|
||||
@ -22,6 +23,7 @@ export default function MoreMenuItem({
|
||||
hrefDownloadName?: string
|
||||
className?: string
|
||||
action?: () => Promise<void> | void
|
||||
dismissMenu?: () => void
|
||||
shouldPreventDefault?: boolean
|
||||
}) {
|
||||
const router = useRouter();
|
||||
@ -30,8 +32,17 @@ export default function MoreMenuItem({
|
||||
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const [transitionDidStart, setTransitionDidStart] = useState(false);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (transitionDidStart && !isPending) {
|
||||
dismissMenu?.();
|
||||
setTransitionDidStart(false);
|
||||
}
|
||||
}, [isPending, dismissMenu, transitionDidStart]);
|
||||
|
||||
return (
|
||||
<DropdownMenu.Item
|
||||
disabled={isLoading}
|
||||
@ -47,21 +58,28 @@ export default function MoreMenuItem({
|
||||
: 'cursor-pointer',
|
||||
className,
|
||||
)}
|
||||
onClick={async e => {
|
||||
onSelect={async e => {
|
||||
if (shouldPreventDefault) { e.preventDefault(); }
|
||||
if (action) {
|
||||
const result = action();
|
||||
if (result instanceof Promise) {
|
||||
setIsLoading(true);
|
||||
await result.finally(() => setIsLoading(false));
|
||||
await result.finally(() => {
|
||||
setIsLoading(false);
|
||||
dismissMenu?.();
|
||||
});
|
||||
}
|
||||
}
|
||||
if (href && href !== pathname) {
|
||||
if (hrefDownloadName) {
|
||||
setIsLoading(true);
|
||||
downloadFileFromBrowser(href, hrefDownloadName)
|
||||
.finally(() => setIsLoading(false));
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
dismissMenu?.();
|
||||
});
|
||||
} else {
|
||||
setTransitionDidStart(true);
|
||||
startTransition(() => router.push(href));
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user