Vercel/src/photo/PhotoPrevNextActions.tsx
2025-04-27 09:30:19 -05:00

232 lines
6.5 KiB
TypeScript

'use client';
import { useCallback, useRef } from 'react';
import {
Photo,
downloadFileNameForPhoto,
getNextPhoto,
getPreviousPhoto,
} from '@/photo';
import { PhotoSetCategory } from '../category';
import PhotoLink from './PhotoLink';
import { pathForAdminPhotoEdit, pathForPhoto } from '@/app/paths';
import { useAppState } from '@/state/AppState';
import { AnimationConfig } from '@/components/AnimateItems';
import { clsx } from 'clsx/lite';
import { FiChevronLeft, FiChevronRight } from 'react-icons/fi';
import useNavigateOrRunActionWithToast
from '@/components/useNavigateOrRunActionWithToast';
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 PhotoPrevNextActions({
photo,
photos = [],
className,
...categories
}: {
photo?: Photo
photos?: Photo[]
className?: string
} & PhotoSetCategory) {
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) { return toggleFavoritePhotoAction(photo.id); }
}, [photo?.id]);
const navigateToPhotoEdit = useNavigateOrRunActionWithToast({
pathOrAction: photo ? pathForAdminPhotoEdit(photo) : undefined,
toastMessage: `Editing ${photoTitle} ...`,
});
const favoritePhoto = useNavigateOrRunActionWithToast({
pathOrAction: toggleFavorite,
toastMessage: `Favoriting ${photoTitle} ...`,
});
const unfavoritePhoto = useNavigateOrRunActionWithToast({
pathOrAction: toggleFavorite,
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);
const previousPhoto = photo ? getPreviousPhoto(photo, photos) : undefined;
const nextPhoto = photo ? getNextPhoto(photo, photos) : undefined;
const pathPrevious = previousPhoto
? pathForPhoto({ photo: previousPhoto, ...categories })
: undefined;
const pathNext = nextPhoto
? pathForPhoto({ photo: nextPhoto, ...categories })
: undefined;
const onKeyDown = useCallback((e: KeyboardEvent) => {
if (e.metaKey) {
switch (e.key.toUpperCase()) {
case KEY_COMMANDS.delete[1]:
if (isUserSignedIn) {
deletePhoto();
}
break;
}
} else {
switch (e.key.toUpperCase()) {
// Public commands
case KEY_COMMANDS.prev[0]:
case KEY_COMMANDS.prev[1]:
if (pathPrevious) {
setNextPhotoAnimation?.(ANIMATION_RIGHT);
refPrevious.current?.click();
}
break;
case KEY_COMMANDS.next[0]:
case KEY_COMMANDS.next[1]:
if (pathNext) {
setNextPhotoAnimation?.(ANIMATION_LEFT);
refNext.current?.click();
}
break;
// Admin commands
case KEY_COMMANDS.edit:
if (isUserSignedIn) {
navigateToPhotoEdit();
}
break;
case KEY_COMMANDS.favorite:
if (isUserSignedIn && photo && !isPhotoFav(photo)) {
favoritePhoto();
}
break;
case KEY_COMMANDS.unfavorite:
if (isUserSignedIn && photo && isPhotoFav(photo)) {
unfavoritePhoto();
}
break;
case KEY_COMMANDS.download:
if (
(isUserSignedIn || ALLOW_PUBLIC_DOWNLOADS) &&
downloadUrl &&
downloadFileName
) {
downloadFileFromBrowser(downloadUrl, downloadFileName);
}
break;
case KEY_COMMANDS.sync:
if (isUserSignedIn) {
syncPhoto();
}
break;
};
}
}, [
setNextPhotoAnimation,
pathPrevious,
pathNext,
isUserSignedIn,
navigateToPhotoEdit,
photo,
favoritePhoto,
unfavoritePhoto,
downloadUrl,
downloadFileName,
syncPhoto,
deletePhoto,
]);
useKeydownHandler({ onKeyDown });
return (
<div className={clsx(
'flex items-center',
className,
)}>
<div className={clsx(
'h-4',
'flex gap-2 select-none',
// Fixes alignment issue when switching from chevrons to text
'items-center sm:items-start',
'*:select-none',
)}>
<Tooltip
content={previousPhoto ? 'Previous' : undefined}
keyCommand={previousPhoto ? KEY_COMMANDS.prev[0] : undefined}
>
<PhotoLink
{...categories}
ref={refPrevious}
photo={previousPhoto}
nextPhotoAnimation={ANIMATION_RIGHT}
scroll={false}
loaderType="badge"
prefetch
>
<FiChevronLeft className="sm:hidden text-[1.1rem]" />
<span className="hidden sm:inline-block">PREV</span>
</PhotoLink>
</Tooltip>
<span className="text-extra-extra-dim">
/
</span>
<Tooltip
content={nextPhoto ? 'Next' : undefined}
keyCommand={nextPhoto ? KEY_COMMANDS.next[0] : undefined}
>
<PhotoLink
{...categories}
ref={refNext}
photo={nextPhoto}
nextPhotoAnimation={ANIMATION_LEFT}
scroll={false}
loaderType="badge"
prefetch
>
<FiChevronRight className="sm:hidden text-[1.1rem]" />
<span className="hidden sm:inline-block">NEXT</span>
</PhotoLink>
</Tooltip>
</div>
</div>
);
};