From 8d91804eb99affdc45c950714a0da35fc0525db6 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sat, 26 Apr 2025 17:32:46 -0500 Subject: [PATCH] Finalize key commands --- src/admin/AdminPhotoMenu.tsx | 16 ++++- src/app/AppViewSwitcher.tsx | 17 +++--- src/components/primitives/KeyCommand.tsx | 24 +++++--- src/photo/PhotoDetailPage.tsx | 1 + src/photo/PhotoHeader.tsx | 4 +- src/photo/PhotoLarge.tsx | 4 +- ...oPrevNext.tsx => PhotoPrevNextActions.tsx} | 61 ++++++++++++++----- src/photo/key-commands.ts | 14 +++++ 8 files changed, 105 insertions(+), 36 deletions(-) rename src/photo/{PhotoPrevNext.tsx => PhotoPrevNextActions.tsx} (78%) create mode 100644 src/photo/key-commands.ts diff --git a/src/admin/AdminPhotoMenu.tsx b/src/admin/AdminPhotoMenu.tsx index 2fcc153c..eae233e2 100644 --- a/src/admin/AdminPhotoMenu.tsx +++ b/src/admin/AdminPhotoMenu.tsx @@ -25,6 +25,7 @@ import InsightsIndicatorDot from './insights/InsightsIndicatorDot'; import IconFavs from '@/components/icons/IconFavs'; import IconEdit from '@/components/icons/IconEdit'; import { photoNeedsToBeSynced } from '@/photo/sync'; +import { KEY_COMMANDS } from '@/photo/key-commands'; export default function AdminPhotoMenu({ photo, @@ -53,7 +54,7 @@ export default function AdminPhotoMenu({ className="translate-x-[0.5px]" />, href: pathForAdminPhotoEdit(photo.id), - ...showKeyCommands && { keyCommand: 'E' }, + ...showKeyCommands && { keyCommand: KEY_COMMANDS.edit }, }]; if (includeFavorite) { sectionMain.push({ @@ -67,7 +68,11 @@ export default function AdminPhotoMenu({ photo.id, shouldRedirectFav, ).then(() => revalidatePhoto?.(photo.id)), - ...showKeyCommands && { keyCommand: isFav ? 'X' : 'P' }, + ...showKeyCommands && { + keyCommand: isFav + ? KEY_COMMANDS.unfavorite + : KEY_COMMANDS.favorite, + }, }); } sectionMain.push({ @@ -78,7 +83,7 @@ export default function AdminPhotoMenu({ />, href: photo.url, hrefDownloadName: downloadFileNameForPhoto(photo), - ...showKeyCommands && { keyCommand: 'D' }, + ...showKeyCommands && { keyCommand: KEY_COMMANDS.download }, }); sectionMain.push({ label: 'Sync', @@ -96,6 +101,7 @@ export default function AdminPhotoMenu({ />, action: () => syncPhotoAction(photo.id) .then(() => revalidatePhoto?.(photo.id)), + ...showKeyCommands && { keyCommand: KEY_COMMANDS.sync }, }); const sectionDelete: ComponentProps[] = [{ label: 'Delete', @@ -117,6 +123,10 @@ export default function AdminPhotoMenu({ }); } }, + ...showKeyCommands && { + keyCommandModifier: KEY_COMMANDS.delete[0], + keyCommand: KEY_COMMANDS.delete[1], + }, }]; return [sectionMain, sectionDelete]; }, [ diff --git a/src/app/AppViewSwitcher.tsx b/src/app/AppViewSwitcher.tsx index 5e3c5018..3ce01e24 100644 --- a/src/app/AppViewSwitcher.tsx +++ b/src/app/AppViewSwitcher.tsx @@ -15,6 +15,7 @@ import clsx from 'clsx/lite'; import { useCallback, useRef, useState } from 'react'; import useKeydownHandler from '@/utility/useKeydownHandler'; import { usePathname } from 'next/navigation'; +import { KEY_COMMANDS } from '@/photo/key-commands'; export type SwitcherSelection = 'feed' | 'grid' | 'admin'; @@ -38,13 +39,13 @@ export default function AppViewSwitcher({ const onKeyDown = useCallback((e: KeyboardEvent) => { switch (e.key.toLocaleUpperCase()) { - case 'F': + case KEY_COMMANDS.feed: if (pathname !== PATH_FEED_INFERRED) { refHrefFeed.current?.click(); } break; - case 'G': + case KEY_COMMANDS.grid: if (pathname !== PATH_GRID_INFERRED) { refHrefGrid.current?.click(); } break; - case 'A': + case KEY_COMMANDS.admin: if (isUserSignedIn) { setIsAdminMenuOpen(true); } break; } @@ -61,7 +62,7 @@ export default function AppViewSwitcher({ active={currentSelection === 'feed'} tooltip={{ content: 'Feed', - keyCommand: 'F', + keyCommand: KEY_COMMANDS.feed, }} noPadding />; @@ -74,7 +75,7 @@ export default function AppViewSwitcher({ active={currentSelection === 'grid'} tooltip={{ content: 'Grid', - keyCommand: 'G', + keyCommand: KEY_COMMANDS.grid, }} noPadding />; @@ -105,7 +106,7 @@ export default function AppViewSwitcher({ />} tooltip={{ content: !isAdminMenuOpen ? 'Admin Menu' : undefined, - keyCommand: !isAdminMenuOpen ? 'A' : undefined, + keyCommand: !isAdminMenuOpen ? KEY_COMMANDS.admin : undefined, }} noPadding />} @@ -116,8 +117,8 @@ export default function AppViewSwitcher({ onClick={() => setIsCommandKOpen?.(true)} tooltip={{ content: 'Search', - keyCommand: 'K', - keyCommandModifier: '⌘', + keyCommandModifier: KEY_COMMANDS.search[0], + keyCommand: KEY_COMMANDS.search[1], }} /> diff --git a/src/components/primitives/KeyCommand.tsx b/src/components/primitives/KeyCommand.tsx index 3630d963..a4260503 100644 --- a/src/components/primitives/KeyCommand.tsx +++ b/src/components/primitives/KeyCommand.tsx @@ -1,5 +1,7 @@ import clsx from 'clsx/lite'; import { useMemo } from 'react'; +import { HiMiniBackspace } from 'react-icons/hi2'; +import { PiCommandBold } from 'react-icons/pi'; export default function KeyCommand({ children, @@ -10,9 +12,12 @@ export default function KeyCommand({ modifier?: '⌘' | '⌥' | '⇧' | '⌃' | '⏎' className?: string }) { - const keys = useMemo(() => - modifier ? [modifier, ...children] : [...children], - [modifier, children]); + const keys = useMemo(() => { + const childrenFormatted = children === 'BACKSPACE' + ? '⌫' + : children; + return modifier ? [modifier, ...childrenFormatted] : [...childrenFormatted]; + }, [modifier, children]); return ( @@ -20,12 +25,17 @@ export default function KeyCommand({ - {key} + {key === '⌘' + ? + : key === '⌫' + ? + : key} ))} diff --git a/src/photo/PhotoDetailPage.tsx b/src/photo/PhotoDetailPage.tsx index f67c0414..251c0404 100644 --- a/src/photo/PhotoDetailPage.tsx +++ b/src/photo/PhotoDetailPage.tsx @@ -139,6 +139,7 @@ export default function PhotoDetailPage({ shouldShareRecipe={recipe !== undefined} shouldShareFocalLength={focal !== undefined} includeFavoriteInAdminMenu={includeFavoriteInAdminMenu} + showAdminKeyCommands />, ]} /> diff --git a/src/photo/PhotoHeader.tsx b/src/photo/PhotoHeader.tsx index 0299bf54..18f2e80e 100644 --- a/src/photo/PhotoHeader.tsx +++ b/src/photo/PhotoHeader.tsx @@ -12,7 +12,7 @@ import ShareButton from '@/share/ShareButton'; import AnimateItems from '@/components/AnimateItems'; import { ReactNode } from 'react'; import DivDebugBaselineGrid from '@/components/DivDebugBaselineGrid'; -import PhotoPrevNext from './PhotoPrevNext'; +import PhotoPrevNextActions from './PhotoPrevNextActions'; import PhotoLink from './PhotoLink'; import ResponsiveText from '@/components/primitives/ResponsiveText'; import { useAppState } from '@/state/AppState'; @@ -59,7 +59,7 @@ export default function PhotoHeader({ : 'photo-detail'; const renderPrevNext = - void + showAdminKeyCommands?: boolean }) { const ref = useRef(null); const refZoomControls = useRef(null); @@ -252,7 +254,7 @@ export default function PhotoLarge({ revalidatePhoto, includeFavorite: includeFavoriteInAdminMenu, ariaLabel: `Admin menu for '${titleForPhoto(photo)}' photo`, - showKeyCommands: true, + showKeyCommands: showAdminKeyCommands, }} />; const largePhotoContainerClassName = clsx( diff --git a/src/photo/PhotoPrevNext.tsx b/src/photo/PhotoPrevNextActions.tsx similarity index 78% rename from src/photo/PhotoPrevNext.tsx rename to src/photo/PhotoPrevNextActions.tsx index e7b6b337..f92298a6 100644 --- a/src/photo/PhotoPrevNext.tsx +++ b/src/photo/PhotoPrevNextActions.tsx @@ -16,17 +16,22 @@ import { clsx } from 'clsx/lite'; import { FiChevronLeft, FiChevronRight } from 'react-icons/fi'; import useNavigateOrRunActionWithToast from '@/components/useNavigateOrRunActionWithToast'; -import { toggleFavoritePhotoAction } from './actions'; +import { + deletePhotoAction, + syncPhotoAction, + toggleFavoritePhotoAction, +} from './actions'; import { isPhotoFav } from '@/tag'; import Tooltip from '@/components/Tooltip'; import { ALLOW_PUBLIC_DOWNLOADS } from '@/app/config'; import { downloadFileFromBrowser } from '@/utility/url'; import useKeydownHandler from '@/utility/useKeydownHandler'; +import { KEY_COMMANDS } from './key-commands'; const ANIMATION_LEFT: AnimationConfig = { type: 'left', duration: 0.3 }; const ANIMATION_RIGHT: AnimationConfig = { type: 'right', duration: 0.3 }; -export default function PhotoPrevNext({ +export default function PhotoPrevNextActions({ photo, photos = [], className, @@ -49,9 +54,7 @@ export default function PhotoPrevNext({ : undefined; const toggleFavorite = useCallback(() => { - if (photo?.id) { - return toggleFavoritePhotoAction(photo.id); - } + if (photo?.id) { return toggleFavoritePhotoAction(photo.id); } }, [photo?.id]); const navigateToPhotoEdit = useNavigateOrRunActionWithToast({ @@ -69,6 +72,22 @@ export default function PhotoPrevNext({ toastMessage: `Unfavoriting ${photoTitle} ...`, }); + const syncPhoto = useNavigateOrRunActionWithToast({ + pathOrAction: useCallback(() => { + if (photo?.id) { return syncPhotoAction(photo.id); } + }, [photo?.id]), + toastMessage: `Syncing ${photoTitle} ...`, + }); + + const deletePhoto = useNavigateOrRunActionWithToast({ + pathOrAction: useCallback(() => { + if (photo?.id && photo.url) { + return deletePhotoAction(photo.id, photo.url, true); + } + }, [photo?.id, photo?.url]), + toastMessage: `Deleting ${photoTitle} ...`, + }); + const refPrevious = useRef(null); const refNext = useRef(null); @@ -85,36 +104,36 @@ export default function PhotoPrevNext({ const onKeyDown = useCallback((e: KeyboardEvent) => { switch (e.key.toUpperCase()) { - case 'ARROWLEFT': - case 'J': + case KEY_COMMANDS.prev[0]: + case KEY_COMMANDS.prev[1]: if (pathPrevious) { setNextPhotoAnimation?.(ANIMATION_RIGHT); refPrevious.current?.click(); } break; - case 'ARROWRIGHT': - case 'L': + case KEY_COMMANDS.next[0]: + case KEY_COMMANDS.next[1]: if (pathNext) { setNextPhotoAnimation?.(ANIMATION_LEFT); refNext.current?.click(); } break; - case 'E': + case KEY_COMMANDS.edit: if (isUserSignedIn) { navigateToPhotoEdit(); } break; - case 'P': + case KEY_COMMANDS.favorite: if (isUserSignedIn && photo && !isPhotoFav(photo)) { favoritePhoto(); } break; - case 'X': + case KEY_COMMANDS.unfavorite: if (isUserSignedIn && photo && isPhotoFav(photo)) { unfavoritePhoto(); } break; - case 'D': + case KEY_COMMANDS.download: if ( (isUserSignedIn || ALLOW_PUBLIC_DOWNLOADS) && downloadUrl && @@ -123,6 +142,16 @@ export default function PhotoPrevNext({ downloadFileFromBrowser(downloadUrl, downloadFileName); } break; + case KEY_COMMANDS.sync: + if (isUserSignedIn) { + syncPhoto(); + } + break; + case KEY_COMMANDS.delete[1]: + if (e.metaKey && isUserSignedIn) { + deletePhoto(); + } + break; }; }, [ setNextPhotoAnimation, @@ -135,6 +164,8 @@ export default function PhotoPrevNext({ unfavoritePhoto, downloadUrl, downloadFileName, + syncPhoto, + deletePhoto, ]); useKeydownHandler({ onKeyDown }); @@ -152,7 +183,7 @@ export default function PhotoPrevNext({ )}>