From 5180ea6276a649dd13b937b446e442be66af8950 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sat, 26 Apr 2025 16:25:51 -0500 Subject: [PATCH] Add key commands to admin photo menu --- src/admin/AdminPhotoMenu.tsx | 6 ++ src/components/more/MoreMenuItem.tsx | 23 +++- .../primitives/TooltipPrimitive.tsx | 4 +- src/photo/PhotoLarge.tsx | 1 + src/photo/PhotoPrevNext.tsx | 102 ++++++++++-------- 5 files changed, 83 insertions(+), 53 deletions(-) diff --git a/src/admin/AdminPhotoMenu.tsx b/src/admin/AdminPhotoMenu.tsx index 394ff54f..2fcc153c 100644 --- a/src/admin/AdminPhotoMenu.tsx +++ b/src/admin/AdminPhotoMenu.tsx @@ -30,11 +30,13 @@ export default function AdminPhotoMenu({ photo, revalidatePhoto, includeFavorite = true, + showKeyCommands, ...props }: Omit, 'sections'> & { photo: Photo revalidatePhoto?: RevalidatePhoto includeFavorite?: boolean + showKeyCommands?: boolean }) { const { isUserSignedIn, registerAdminUpdate } = useAppState(); @@ -51,6 +53,7 @@ export default function AdminPhotoMenu({ className="translate-x-[0.5px]" />, href: pathForAdminPhotoEdit(photo.id), + ...showKeyCommands && { keyCommand: 'E' }, }]; if (includeFavorite) { sectionMain.push({ @@ -64,6 +67,7 @@ export default function AdminPhotoMenu({ photo.id, shouldRedirectFav, ).then(() => revalidatePhoto?.(photo.id)), + ...showKeyCommands && { keyCommand: isFav ? 'X' : 'P' }, }); } sectionMain.push({ @@ -74,6 +78,7 @@ export default function AdminPhotoMenu({ />, href: photo.url, hrefDownloadName: downloadFileNameForPhoto(photo), + ...showKeyCommands && { keyCommand: 'D' }, }); sectionMain.push({ label: 'Sync', @@ -116,6 +121,7 @@ export default function AdminPhotoMenu({ return [sectionMain, sectionDelete]; }, [ photo, + showKeyCommands, includeFavorite, isFav, shouldRedirectFav, diff --git a/src/components/more/MoreMenuItem.tsx b/src/components/more/MoreMenuItem.tsx index fe7eaeef..24534570 100644 --- a/src/components/more/MoreMenuItem.tsx +++ b/src/components/more/MoreMenuItem.tsx @@ -2,10 +2,17 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; import { clsx } from 'clsx/lite'; -import { ReactNode, useEffect, useState, useTransition } from 'react'; +import { + ComponentProps, + ReactNode, + useEffect, + useState, + useTransition, +} from 'react'; import LoaderButton from '../primitives/LoaderButton'; import { usePathname, useRouter } from 'next/navigation'; import { downloadFileFromBrowser } from '@/utility/url'; +import KeyCommand from '../primitives/KeyCommand'; export default function MoreMenuItem({ label, @@ -19,6 +26,8 @@ export default function MoreMenuItem({ action, dismissMenu, shouldPreventDefault = true, + keyCommand, + keyCommandModifier, }: { label: string labelComplex?: ReactNode @@ -31,6 +40,8 @@ export default function MoreMenuItem({ action?: () => Promise | void dismissMenu?: () => void shouldPreventDefault?: boolean + keyCommand?: string + keyCommandModifier?: ComponentProps['modifier'] }) { const router = useRouter(); @@ -66,8 +77,8 @@ export default function MoreMenuItem({ @@ -133,6 +144,10 @@ export default function MoreMenuItem({ {annotation} } + {keyCommand && + + {keyCommand} + } ); } diff --git a/src/components/primitives/TooltipPrimitive.tsx b/src/components/primitives/TooltipPrimitive.tsx index bce0ea5d..7673546e 100644 --- a/src/components/primitives/TooltipPrimitive.tsx +++ b/src/components/primitives/TooltipPrimitive.tsx @@ -9,6 +9,7 @@ import useClickInsideOutside from '@/utility/useClickInsideOutside'; import KeyCommand from './KeyCommand'; export default function TooltipPrimitive({ content: contentProp, + children, className, classNameTrigger: classNameTriggerProp, sideOffset = 10, @@ -18,9 +19,9 @@ export default function TooltipPrimitive({ color, keyCommand, keyCommandModifier, - children, }: { content?: ReactNode + children: ReactNode className?: string classNameTrigger?: string sideOffset?: number @@ -30,7 +31,6 @@ export default function TooltipPrimitive({ color?: ComponentProps['color'] keyCommand?: string keyCommandModifier?: ComponentProps['modifier'] - children: ReactNode }) { const refTrigger = useRef(null); const refContent = useRef(null); diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index 5b55d7c6..b412c853 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -252,6 +252,7 @@ export default function PhotoLarge({ revalidatePhoto, includeFavorite: includeFavoriteInAdminMenu, ariaLabel: `Admin menu for '${titleForPhoto(photo)}' photo`, + showKeyCommands: true, }} />; const largePhotoContainerClassName = clsx( diff --git a/src/photo/PhotoPrevNext.tsx b/src/photo/PhotoPrevNext.tsx index b3dfc4f9..e7b6b337 100644 --- a/src/photo/PhotoPrevNext.tsx +++ b/src/photo/PhotoPrevNext.tsx @@ -1,8 +1,9 @@ 'use client'; -import { useCallback, useEffect, useRef } from 'react'; +import { useCallback, useRef } from 'react'; import { Photo, + downloadFileNameForPhoto, getNextPhoto, getPreviousPhoto, } from '@/photo'; @@ -18,8 +19,9 @@ import useNavigateOrRunActionWithToast import { toggleFavoritePhotoAction } from './actions'; import { isPhotoFav } from '@/tag'; import Tooltip from '@/components/Tooltip'; - -const LISTENER_KEYUP = 'keyup'; +import { ALLOW_PUBLIC_DOWNLOADS } from '@/app/config'; +import { downloadFileFromBrowser } from '@/utility/url'; +import useKeydownHandler from '@/utility/useKeydownHandler'; const ANIMATION_LEFT: AnimationConfig = { type: 'left', duration: 0.3 }; const ANIMATION_RIGHT: AnimationConfig = { type: 'right', duration: 0.3 }; @@ -34,17 +36,17 @@ export default function PhotoPrevNext({ photos?: Photo[] className?: string } & PhotoSetCategory) { - const { - setNextPhotoAnimation, - shouldRespondToKeyboardCommands, - isUserSignedIn, - } = useAppState(); + const { setNextPhotoAnimation, isUserSignedIn } = useAppState(); const photoTitle = photo ? photo.title ? `'${photo.title}'` : 'photo' : undefined; + const downloadUrl = photo?.url; + const downloadFileName = photo + ? downloadFileNameForPhoto(photo) + : undefined; const toggleFavorite = useCallback(() => { if (photo?.id) { @@ -81,55 +83,61 @@ export default function PhotoPrevNext({ ? pathForPhoto({ photo: nextPhoto, ...categories }) : undefined; - useEffect(() => { - if (shouldRespondToKeyboardCommands) { - const onKeyUp = (e: KeyboardEvent) => { - switch (e.key.toUpperCase()) { - case 'ARROWLEFT': - case 'J': - if (pathPrevious) { - setNextPhotoAnimation?.(ANIMATION_RIGHT); - refPrevious.current?.click(); - } - break; - case 'ARROWRIGHT': - case 'L': - if (pathNext) { - setNextPhotoAnimation?.(ANIMATION_LEFT); - refNext.current?.click(); - } - break; - case 'E': - if (isUserSignedIn) { navigateToPhotoEdit(); } - break; - case 'P': - if (isUserSignedIn && photo && !isPhotoFav(photo)) { - favoritePhoto(); - } - break; - case 'X': - if (isUserSignedIn && photo && isPhotoFav(photo)) { - unfavoritePhoto(); - } - break; - }; - }; - window.addEventListener(LISTENER_KEYUP, onKeyUp); - return () => window.removeEventListener(LISTENER_KEYUP, onKeyUp); - } + const onKeyDown = useCallback((e: KeyboardEvent) => { + switch (e.key.toUpperCase()) { + case 'ARROWLEFT': + case 'J': + if (pathPrevious) { + setNextPhotoAnimation?.(ANIMATION_RIGHT); + refPrevious.current?.click(); + } + break; + case 'ARROWRIGHT': + case 'L': + if (pathNext) { + setNextPhotoAnimation?.(ANIMATION_LEFT); + refNext.current?.click(); + } + break; + case 'E': + if (isUserSignedIn) { + navigateToPhotoEdit(); + } + break; + case 'P': + if (isUserSignedIn && photo && !isPhotoFav(photo)) { + favoritePhoto(); + } + break; + case 'X': + if (isUserSignedIn && photo && isPhotoFav(photo)) { + unfavoritePhoto(); + } + break; + case 'D': + if ( + (isUserSignedIn || ALLOW_PUBLIC_DOWNLOADS) && + downloadUrl && + downloadFileName + ) { + downloadFileFromBrowser(downloadUrl, downloadFileName); + } + break; + }; }, [ - shouldRespondToKeyboardCommands, setNextPhotoAnimation, pathPrevious, pathNext, isUserSignedIn, - photoTitle, navigateToPhotoEdit, photo, favoritePhoto, unfavoritePhoto, + downloadUrl, + downloadFileName, ]); - + useKeydownHandler({ onKeyDown }); + return (