Add key commands to admin photo menu
This commit is contained in:
parent
9bf0665243
commit
5180ea6276
@ -30,11 +30,13 @@ export default function AdminPhotoMenu({
|
|||||||
photo,
|
photo,
|
||||||
revalidatePhoto,
|
revalidatePhoto,
|
||||||
includeFavorite = true,
|
includeFavorite = true,
|
||||||
|
showKeyCommands,
|
||||||
...props
|
...props
|
||||||
}: Omit<ComponentProps<typeof MoreMenu>, 'sections'> & {
|
}: Omit<ComponentProps<typeof MoreMenu>, 'sections'> & {
|
||||||
photo: Photo
|
photo: Photo
|
||||||
revalidatePhoto?: RevalidatePhoto
|
revalidatePhoto?: RevalidatePhoto
|
||||||
includeFavorite?: boolean
|
includeFavorite?: boolean
|
||||||
|
showKeyCommands?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { isUserSignedIn, registerAdminUpdate } = useAppState();
|
const { isUserSignedIn, registerAdminUpdate } = useAppState();
|
||||||
|
|
||||||
@ -51,6 +53,7 @@ export default function AdminPhotoMenu({
|
|||||||
className="translate-x-[0.5px]"
|
className="translate-x-[0.5px]"
|
||||||
/>,
|
/>,
|
||||||
href: pathForAdminPhotoEdit(photo.id),
|
href: pathForAdminPhotoEdit(photo.id),
|
||||||
|
...showKeyCommands && { keyCommand: 'E' },
|
||||||
}];
|
}];
|
||||||
if (includeFavorite) {
|
if (includeFavorite) {
|
||||||
sectionMain.push({
|
sectionMain.push({
|
||||||
@ -64,6 +67,7 @@ export default function AdminPhotoMenu({
|
|||||||
photo.id,
|
photo.id,
|
||||||
shouldRedirectFav,
|
shouldRedirectFav,
|
||||||
).then(() => revalidatePhoto?.(photo.id)),
|
).then(() => revalidatePhoto?.(photo.id)),
|
||||||
|
...showKeyCommands && { keyCommand: isFav ? 'X' : 'P' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
sectionMain.push({
|
sectionMain.push({
|
||||||
@ -74,6 +78,7 @@ export default function AdminPhotoMenu({
|
|||||||
/>,
|
/>,
|
||||||
href: photo.url,
|
href: photo.url,
|
||||||
hrefDownloadName: downloadFileNameForPhoto(photo),
|
hrefDownloadName: downloadFileNameForPhoto(photo),
|
||||||
|
...showKeyCommands && { keyCommand: 'D' },
|
||||||
});
|
});
|
||||||
sectionMain.push({
|
sectionMain.push({
|
||||||
label: 'Sync',
|
label: 'Sync',
|
||||||
@ -116,6 +121,7 @@ export default function AdminPhotoMenu({
|
|||||||
return [sectionMain, sectionDelete];
|
return [sectionMain, sectionDelete];
|
||||||
}, [
|
}, [
|
||||||
photo,
|
photo,
|
||||||
|
showKeyCommands,
|
||||||
includeFavorite,
|
includeFavorite,
|
||||||
isFav,
|
isFav,
|
||||||
shouldRedirectFav,
|
shouldRedirectFav,
|
||||||
|
|||||||
@ -2,10 +2,17 @@
|
|||||||
|
|
||||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||||
import { clsx } from 'clsx/lite';
|
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 LoaderButton from '../primitives/LoaderButton';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import { downloadFileFromBrowser } from '@/utility/url';
|
import { downloadFileFromBrowser } from '@/utility/url';
|
||||||
|
import KeyCommand from '../primitives/KeyCommand';
|
||||||
|
|
||||||
export default function MoreMenuItem({
|
export default function MoreMenuItem({
|
||||||
label,
|
label,
|
||||||
@ -19,6 +26,8 @@ export default function MoreMenuItem({
|
|||||||
action,
|
action,
|
||||||
dismissMenu,
|
dismissMenu,
|
||||||
shouldPreventDefault = true,
|
shouldPreventDefault = true,
|
||||||
|
keyCommand,
|
||||||
|
keyCommandModifier,
|
||||||
}: {
|
}: {
|
||||||
label: string
|
label: string
|
||||||
labelComplex?: ReactNode
|
labelComplex?: ReactNode
|
||||||
@ -31,6 +40,8 @@ export default function MoreMenuItem({
|
|||||||
action?: () => Promise<void | boolean> | void
|
action?: () => Promise<void | boolean> | void
|
||||||
dismissMenu?: () => void
|
dismissMenu?: () => void
|
||||||
shouldPreventDefault?: boolean
|
shouldPreventDefault?: boolean
|
||||||
|
keyCommand?: string
|
||||||
|
keyCommandModifier?: ComponentProps<typeof KeyCommand>['modifier']
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@ -66,8 +77,8 @@ export default function MoreMenuItem({
|
|||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex items-center h-8.5',
|
'flex items-center h-8.5 gap-4',
|
||||||
'pl-2 pr-3 py-2 rounded-sm',
|
'px-2 py-2 rounded-sm',
|
||||||
'select-none hover:outline-hidden',
|
'select-none hover:outline-hidden',
|
||||||
getColorClasses(),
|
getColorClasses(),
|
||||||
'whitespace-nowrap',
|
'whitespace-nowrap',
|
||||||
@ -122,7 +133,7 @@ export default function MoreMenuItem({
|
|||||||
isLoading={isLoading || isPending}
|
isLoading={isLoading || isPending}
|
||||||
hideTextOnMobile={false}
|
hideTextOnMobile={false}
|
||||||
styleAs="link-without-hover"
|
styleAs="link-without-hover"
|
||||||
className="translate-y-[0.5px] text-sm"
|
className="translate-y-[0.5px] text-sm grow"
|
||||||
classNameIcon="translate-y-[-0.5px]!"
|
classNameIcon="translate-y-[-0.5px]!"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
@ -133,6 +144,10 @@ export default function MoreMenuItem({
|
|||||||
{annotation}
|
{annotation}
|
||||||
</span>}
|
</span>}
|
||||||
</LoaderButton>
|
</LoaderButton>
|
||||||
|
{keyCommand &&
|
||||||
|
<KeyCommand modifier={keyCommandModifier}>
|
||||||
|
{keyCommand}
|
||||||
|
</KeyCommand>}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import useClickInsideOutside from '@/utility/useClickInsideOutside';
|
|||||||
import KeyCommand from './KeyCommand';
|
import KeyCommand from './KeyCommand';
|
||||||
export default function TooltipPrimitive({
|
export default function TooltipPrimitive({
|
||||||
content: contentProp,
|
content: contentProp,
|
||||||
|
children,
|
||||||
className,
|
className,
|
||||||
classNameTrigger: classNameTriggerProp,
|
classNameTrigger: classNameTriggerProp,
|
||||||
sideOffset = 10,
|
sideOffset = 10,
|
||||||
@ -18,9 +19,9 @@ export default function TooltipPrimitive({
|
|||||||
color,
|
color,
|
||||||
keyCommand,
|
keyCommand,
|
||||||
keyCommandModifier,
|
keyCommandModifier,
|
||||||
children,
|
|
||||||
}: {
|
}: {
|
||||||
content?: ReactNode
|
content?: ReactNode
|
||||||
|
children: ReactNode
|
||||||
className?: string
|
className?: string
|
||||||
classNameTrigger?: string
|
classNameTrigger?: string
|
||||||
sideOffset?: number
|
sideOffset?: number
|
||||||
@ -30,7 +31,6 @@ export default function TooltipPrimitive({
|
|||||||
color?: ComponentProps<typeof MenuSurface>['color']
|
color?: ComponentProps<typeof MenuSurface>['color']
|
||||||
keyCommand?: string
|
keyCommand?: string
|
||||||
keyCommandModifier?: ComponentProps<typeof KeyCommand>['modifier']
|
keyCommandModifier?: ComponentProps<typeof KeyCommand>['modifier']
|
||||||
children: ReactNode
|
|
||||||
}) {
|
}) {
|
||||||
const refTrigger = useRef<HTMLButtonElement>(null);
|
const refTrigger = useRef<HTMLButtonElement>(null);
|
||||||
const refContent = useRef<HTMLDivElement>(null);
|
const refContent = useRef<HTMLDivElement>(null);
|
||||||
|
|||||||
@ -252,6 +252,7 @@ export default function PhotoLarge({
|
|||||||
revalidatePhoto,
|
revalidatePhoto,
|
||||||
includeFavorite: includeFavoriteInAdminMenu,
|
includeFavorite: includeFavoriteInAdminMenu,
|
||||||
ariaLabel: `Admin menu for '${titleForPhoto(photo)}' photo`,
|
ariaLabel: `Admin menu for '${titleForPhoto(photo)}' photo`,
|
||||||
|
showKeyCommands: true,
|
||||||
}} />;
|
}} />;
|
||||||
|
|
||||||
const largePhotoContainerClassName = clsx(
|
const largePhotoContainerClassName = clsx(
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
Photo,
|
Photo,
|
||||||
|
downloadFileNameForPhoto,
|
||||||
getNextPhoto,
|
getNextPhoto,
|
||||||
getPreviousPhoto,
|
getPreviousPhoto,
|
||||||
} from '@/photo';
|
} from '@/photo';
|
||||||
@ -18,8 +19,9 @@ import useNavigateOrRunActionWithToast
|
|||||||
import { toggleFavoritePhotoAction } from './actions';
|
import { toggleFavoritePhotoAction } from './actions';
|
||||||
import { isPhotoFav } from '@/tag';
|
import { isPhotoFav } from '@/tag';
|
||||||
import Tooltip from '@/components/Tooltip';
|
import Tooltip from '@/components/Tooltip';
|
||||||
|
import { ALLOW_PUBLIC_DOWNLOADS } from '@/app/config';
|
||||||
const LISTENER_KEYUP = 'keyup';
|
import { downloadFileFromBrowser } from '@/utility/url';
|
||||||
|
import useKeydownHandler from '@/utility/useKeydownHandler';
|
||||||
|
|
||||||
const ANIMATION_LEFT: AnimationConfig = { type: 'left', duration: 0.3 };
|
const ANIMATION_LEFT: AnimationConfig = { type: 'left', duration: 0.3 };
|
||||||
const ANIMATION_RIGHT: AnimationConfig = { type: 'right', duration: 0.3 };
|
const ANIMATION_RIGHT: AnimationConfig = { type: 'right', duration: 0.3 };
|
||||||
@ -34,17 +36,17 @@ export default function PhotoPrevNext({
|
|||||||
photos?: Photo[]
|
photos?: Photo[]
|
||||||
className?: string
|
className?: string
|
||||||
} & PhotoSetCategory) {
|
} & PhotoSetCategory) {
|
||||||
const {
|
const { setNextPhotoAnimation, isUserSignedIn } = useAppState();
|
||||||
setNextPhotoAnimation,
|
|
||||||
shouldRespondToKeyboardCommands,
|
|
||||||
isUserSignedIn,
|
|
||||||
} = useAppState();
|
|
||||||
|
|
||||||
const photoTitle = photo
|
const photoTitle = photo
|
||||||
? photo.title
|
? photo.title
|
||||||
? `'${photo.title}'`
|
? `'${photo.title}'`
|
||||||
: 'photo'
|
: 'photo'
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const downloadUrl = photo?.url;
|
||||||
|
const downloadFileName = photo
|
||||||
|
? downloadFileNameForPhoto(photo)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const toggleFavorite = useCallback(() => {
|
const toggleFavorite = useCallback(() => {
|
||||||
if (photo?.id) {
|
if (photo?.id) {
|
||||||
@ -81,54 +83,60 @@ export default function PhotoPrevNext({
|
|||||||
? pathForPhoto({ photo: nextPhoto, ...categories })
|
? pathForPhoto({ photo: nextPhoto, ...categories })
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
useEffect(() => {
|
const onKeyDown = useCallback((e: KeyboardEvent) => {
|
||||||
if (shouldRespondToKeyboardCommands) {
|
switch (e.key.toUpperCase()) {
|
||||||
const onKeyUp = (e: KeyboardEvent) => {
|
case 'ARROWLEFT':
|
||||||
switch (e.key.toUpperCase()) {
|
case 'J':
|
||||||
case 'ARROWLEFT':
|
if (pathPrevious) {
|
||||||
case 'J':
|
setNextPhotoAnimation?.(ANIMATION_RIGHT);
|
||||||
if (pathPrevious) {
|
refPrevious.current?.click();
|
||||||
setNextPhotoAnimation?.(ANIMATION_RIGHT);
|
}
|
||||||
refPrevious.current?.click();
|
break;
|
||||||
}
|
case 'ARROWRIGHT':
|
||||||
break;
|
case 'L':
|
||||||
case 'ARROWRIGHT':
|
if (pathNext) {
|
||||||
case 'L':
|
setNextPhotoAnimation?.(ANIMATION_LEFT);
|
||||||
if (pathNext) {
|
refNext.current?.click();
|
||||||
setNextPhotoAnimation?.(ANIMATION_LEFT);
|
}
|
||||||
refNext.current?.click();
|
break;
|
||||||
}
|
case 'E':
|
||||||
break;
|
if (isUserSignedIn) {
|
||||||
case 'E':
|
navigateToPhotoEdit();
|
||||||
if (isUserSignedIn) { navigateToPhotoEdit(); }
|
}
|
||||||
break;
|
break;
|
||||||
case 'P':
|
case 'P':
|
||||||
if (isUserSignedIn && photo && !isPhotoFav(photo)) {
|
if (isUserSignedIn && photo && !isPhotoFav(photo)) {
|
||||||
favoritePhoto();
|
favoritePhoto();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'X':
|
case 'X':
|
||||||
if (isUserSignedIn && photo && isPhotoFav(photo)) {
|
if (isUserSignedIn && photo && isPhotoFav(photo)) {
|
||||||
unfavoritePhoto();
|
unfavoritePhoto();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
};
|
case 'D':
|
||||||
};
|
if (
|
||||||
window.addEventListener(LISTENER_KEYUP, onKeyUp);
|
(isUserSignedIn || ALLOW_PUBLIC_DOWNLOADS) &&
|
||||||
return () => window.removeEventListener(LISTENER_KEYUP, onKeyUp);
|
downloadUrl &&
|
||||||
}
|
downloadFileName
|
||||||
|
) {
|
||||||
|
downloadFileFromBrowser(downloadUrl, downloadFileName);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
};
|
||||||
}, [
|
}, [
|
||||||
shouldRespondToKeyboardCommands,
|
|
||||||
setNextPhotoAnimation,
|
setNextPhotoAnimation,
|
||||||
pathPrevious,
|
pathPrevious,
|
||||||
pathNext,
|
pathNext,
|
||||||
isUserSignedIn,
|
isUserSignedIn,
|
||||||
photoTitle,
|
|
||||||
navigateToPhotoEdit,
|
navigateToPhotoEdit,
|
||||||
photo,
|
photo,
|
||||||
favoritePhoto,
|
favoritePhoto,
|
||||||
unfavoritePhoto,
|
unfavoritePhoto,
|
||||||
|
downloadUrl,
|
||||||
|
downloadFileName,
|
||||||
]);
|
]);
|
||||||
|
useKeydownHandler({ onKeyDown });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user