Finalize key commands

This commit is contained in:
Sam Becker 2025-04-26 17:32:46 -05:00
parent 5180ea6276
commit 8d91804eb9
8 changed files with 105 additions and 36 deletions

View File

@ -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<typeof MoreMenuItem>[] = [{
label: 'Delete',
@ -117,6 +123,10 @@ export default function AdminPhotoMenu({
});
}
},
...showKeyCommands && {
keyCommandModifier: KEY_COMMANDS.delete[0],
keyCommand: KEY_COMMANDS.delete[1],
},
}];
return [sectionMain, sectionDelete];
}, [

View File

@ -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],
}}
/>
</Switcher>

View File

@ -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 (
<span className={clsx('inline-flex items-center gap-0.5', className)}>
@ -20,12 +25,17 @@ export default function KeyCommand({
<span
key={key}
className={clsx(
'px-1 rounded-sm shadow-xs',
'text-gray-600 bg-gray-200/90',
'dark:text-gray-300 dark:bg-gray-600/55',
'inline-flex items-center justify-center',
'px-1 h-4 rounded-sm text-xs font-medium',
'text-gray-500/90 bg-gray-200/70',
'dark:text-gray-300/90 dark:bg-gray-600/50',
)}
>
{key}
{key === '⌘'
? <PiCommandBold />
: key === '⌫'
? <HiMiniBackspace className="text-[13px]" />
: key}
</span>
))}
</span>

View File

@ -139,6 +139,7 @@ export default function PhotoDetailPage({
shouldShareRecipe={recipe !== undefined}
shouldShareFocalLength={focal !== undefined}
includeFavoriteInAdminMenu={includeFavoriteInAdminMenu}
showAdminKeyCommands
/>,
]}
/>

View File

@ -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 =
<PhotoPrevNext {...{
<PhotoPrevNextActions {...{
photo: selectedPhoto,
photos,
...categories,

View File

@ -76,6 +76,7 @@ export default function PhotoLarge({
shouldShareFocalLength,
includeFavoriteInAdminMenu,
onVisible,
showAdminKeyCommands,
}: {
photo: Photo
className?: string
@ -101,6 +102,7 @@ export default function PhotoLarge({
shouldShareFocalLength?: boolean
includeFavoriteInAdminMenu?: boolean
onVisible?: () => void
showAdminKeyCommands?: boolean
}) {
const ref = useRef<HTMLDivElement>(null);
const refZoomControls = useRef<ZoomControlsRef>(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(

View File

@ -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<HTMLAnchorElement | null>(null);
const refNext = useRef<HTMLAnchorElement | null>(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({
)}>
<Tooltip
content={previousPhoto ? 'Previous' : undefined}
keyCommand="J"
keyCommand={previousPhoto ? KEY_COMMANDS.prev[0] : undefined}
>
<PhotoLink
{...categories}
@ -172,7 +203,7 @@ export default function PhotoPrevNext({
</span>
<Tooltip
content={nextPhoto ? 'Next' : undefined}
keyCommand="L"
keyCommand={nextPhoto ? KEY_COMMANDS.next[0] : undefined}
>
<PhotoLink
{...categories}

14
src/photo/key-commands.ts Normal file
View File

@ -0,0 +1,14 @@
export const KEY_COMMANDS = {
feed: 'F',
grid: 'G',
admin: 'A',
prev: ['J', 'ARROWLEFT'],
next: ['L', 'ARROWRIGHT'],
edit: 'E',
favorite: 'P',
unfavorite: 'X',
download: 'D',
sync: 'S',
search: ['⌘', 'K'],
delete: ['⌘', 'BACKSPACE'],
} as const;