From 9f483bcf213228662b4983219f621fc3608071a1 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 25 Feb 2025 09:13:43 -0600 Subject: [PATCH] Create multi-item admin menu --- src/admin/AdminAppMenu.tsx | 51 +++++++++++++++++---- src/admin/AdminPhotoMenuClient.tsx | 5 +- src/admin/insights/AdminAppInsightsIcon.tsx | 11 ++++- src/app/Nav.tsx | 15 ------ src/app/ViewSwitcher.tsx | 24 ++++++---- src/components/Switcher.tsx | 4 +- src/components/more/MoreMenu.tsx | 38 +++++++++++---- src/components/more/MoreMenuItem.tsx | 26 +++++++++-- 8 files changed, 124 insertions(+), 50 deletions(-) diff --git a/src/admin/AdminAppMenu.tsx b/src/admin/AdminAppMenu.tsx index d7c1cf3c..ea2b63c2 100644 --- a/src/admin/AdminAppMenu.tsx +++ b/src/admin/AdminAppMenu.tsx @@ -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 ( } + align="start" + className={clsx( + 'border-medium', + className, + )} + buttonClassName={clsx( + 'rounded-none focus:outline-none', + buttonClassName, + )} items={[{ + label: 'Manage Photos', + icon: , + href: PATH_ADMIN_PHOTOS, + }, { + label: 'Manage Tags', + icon: , + href: PATH_ADMIN_TAGS, + }, { label: 'Insights', - icon: - - , + icon: , href: PATH_ADMIN_INSIGHTS, }, { label: 'Configuration', icon: , href: PATH_ADMIN_CONFIGURATION, }, { label: isSelecting ? 'Exit Select' - : 'Select', + : 'Select Photos', icon: isSelecting ? : , href: PATH_GRID_INFERRED, action: () => { diff --git a/src/admin/AdminPhotoMenuClient.tsx b/src/admin/AdminPhotoMenuClient.tsx index 78f14b0b..0cd75923 100644 --- a/src/admin/AdminPhotoMenuClient.tsx +++ b/src/admin/AdminPhotoMenuClient.tsx @@ -47,7 +47,10 @@ export default function AdminPhotoMenuClient({ const items = useMemo(() => { const items: ComponentProps[] = [{ label: 'Edit', - icon: , + icon: , href: pathForAdminPhotoEdit(photo.id), }]; if (includeFavorite) { diff --git a/src/admin/insights/AdminAppInsightsIcon.tsx b/src/admin/insights/AdminAppInsightsIcon.tsx index b2b9578a..19d91380 100644 --- a/src/admin/insights/AdminAppInsightsIcon.tsx +++ b/src/admin/insights/AdminAppInsightsIcon.tsx @@ -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 ( - +
} - contentSide={isUserSignedIn && !isPathAdmin(pathname) - ?
- -
- : undefined} sideHiddenOnMobile /> ); diff --git a/src/app/ViewSwitcher.tsx b/src/app/ViewSwitcher.tsx index 2ff538ed..407ade46 100644 --- a/src/app/ViewSwitcher.tsx +++ b/src/app/ViewSwitcher.tsx @@ -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 = () => {GRID_HOMEPAGE_ENABLED ? renderItemGrid() : renderItemFeed()} {GRID_HOMEPAGE_ENABLED ? renderItemFeed() : renderItemGrid()} - {showAdmin && - } - href={PATH_ADMIN_PHOTOS} - active={currentSelection === 'admin'} + {isUserSignedIn && + } diff --git a/src/components/Switcher.tsx b/src/components/Switcher.tsx index a5225b89..40fd7b06 100644 --- a/src/components/Switcher.tsx +++ b/src/components/Switcher.tsx @@ -11,10 +11,10 @@ export default function Switcher({ return (
diff --git a/src/components/more/MoreMenu.tsx b/src/components/more/MoreMenu.tsx index 81f8b8dc..968c26d3 100644 --- a/src/components/more/MoreMenu.tsx +++ b/src/components/more/MoreMenu.tsx @@ -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 [] + icon?: ReactNode + header?: ReactNode className?: string buttonClassName?: string ariaLabel: string -}){ +} & ComponentProps){ + const [isOpen, setIsOpen] = useState(false); + + const dismissMenu = useCallback(() => { + setIsOpen(false); + }, [setIsOpen]); + return ( - + + {header &&
+ {header} +
} {items.map(props => - , + , )}
diff --git a/src/components/more/MoreMenuItem.tsx b/src/components/more/MoreMenuItem.tsx index cc2c904d..122549f9 100644 --- a/src/components/more/MoreMenuItem.tsx +++ b/src/components/more/MoreMenuItem.tsx @@ -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 + 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 ( { + 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)); } }