Refine admin photo menu placement/appearance
This commit is contained in:
parent
0f1753fad0
commit
477d7c088e
@ -1,12 +1,131 @@
|
||||
import { authCachedSafe } from '@/auth/cache';
|
||||
import AdminPhotoMenuClient from './AdminPhotoMenuClient';
|
||||
import { ComponentProps } from 'react';
|
||||
'use client';
|
||||
|
||||
export default async function AdminPhotoMenu(
|
||||
props: ComponentProps<typeof AdminPhotoMenuClient>,
|
||||
) {
|
||||
const session = await authCachedSafe();
|
||||
return Boolean(session?.user?.email)
|
||||
? <AdminPhotoMenuClient {...props} />
|
||||
: null;
|
||||
import { ComponentProps, useMemo } from 'react';
|
||||
import { pathForAdminPhotoEdit, pathForPhoto } from '@/app/paths';
|
||||
import {
|
||||
deletePhotoAction,
|
||||
syncPhotoAction,
|
||||
toggleFavoritePhotoAction,
|
||||
} from '@/photo/actions';
|
||||
import {
|
||||
Photo,
|
||||
deleteConfirmationTextForPhoto,
|
||||
downloadFileNameForPhoto,
|
||||
} from '@/photo';
|
||||
import { isPathFavs, isPhotoFav } from '@/tag';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { BiTrash } from 'react-icons/bi';
|
||||
import MoreMenu from '@/components/more/MoreMenu';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import { RevalidatePhoto } from '@/photo/InfinitePhotoScroll';
|
||||
import { MdOutlineFileDownload } from 'react-icons/md';
|
||||
import MoreMenuItem from '@/components/more/MoreMenuItem';
|
||||
import IconGrSync from '@/components/icons/IconGrSync';
|
||||
import { isPhotoOutdated } from '@/photo/outdated';
|
||||
import InsightsIndicatorDot from './insights/InsightsIndicatorDot';
|
||||
import IconFavs from '@/components/icons/IconFavs';
|
||||
import IconEdit from '@/components/icons/IconEdit';
|
||||
|
||||
export default function AdminPhotoMenu({
|
||||
photo,
|
||||
revalidatePhoto,
|
||||
includeFavorite = true,
|
||||
...props
|
||||
}: Omit<ComponentProps<typeof MoreMenu>, 'items'> & {
|
||||
photo: Photo
|
||||
revalidatePhoto?: RevalidatePhoto
|
||||
includeFavorite?: boolean
|
||||
}) {
|
||||
const { isUserSignedIn, registerAdminUpdate } = useAppState();
|
||||
|
||||
const isFav = isPhotoFav(photo);
|
||||
const path = usePathname();
|
||||
const shouldRedirectFav = isPathFavs(path) && isFav;
|
||||
const shouldRedirectDelete = pathForPhoto({ photo: photo.id }) === path;
|
||||
|
||||
const items = useMemo(() => {
|
||||
const items: ComponentProps<typeof MoreMenuItem>[] = [{
|
||||
label: 'Edit',
|
||||
icon: <IconEdit
|
||||
size={15}
|
||||
className="translate-x-[0.5px] translate-y-[-0.5px]"
|
||||
/>,
|
||||
href: pathForAdminPhotoEdit(photo.id),
|
||||
}];
|
||||
if (includeFavorite) {
|
||||
items.push({
|
||||
label: isFav ? 'Unfavorite' : 'Favorite',
|
||||
icon: <IconFavs
|
||||
size={14}
|
||||
className="translate-x-[-1px]"
|
||||
highlight={isFav}
|
||||
/>,
|
||||
action: () => toggleFavoritePhotoAction(
|
||||
photo.id,
|
||||
shouldRedirectFav,
|
||||
).then(() => revalidatePhoto?.(photo.id)),
|
||||
});
|
||||
}
|
||||
items.push({
|
||||
label: 'Download',
|
||||
icon: <MdOutlineFileDownload
|
||||
size={17}
|
||||
className="translate-x-[-1px] translate-y-[-0.5px]"
|
||||
/>,
|
||||
href: photo.url,
|
||||
hrefDownloadName: downloadFileNameForPhoto(photo),
|
||||
});
|
||||
items.push({
|
||||
label: 'Sync',
|
||||
labelComplex: <span className="inline-flex items-center gap-2">
|
||||
<span>Sync</span>
|
||||
{isPhotoOutdated(photo) &&
|
||||
<InsightsIndicatorDot
|
||||
colorOverride="blue"
|
||||
className="translate-y-[1.5px]"
|
||||
/>}
|
||||
</span>,
|
||||
icon: <IconGrSync className="translate-x-[-1px]" />,
|
||||
action: () => syncPhotoAction(photo.id)
|
||||
.then(() => revalidatePhoto?.(photo.id)),
|
||||
});
|
||||
items.push({
|
||||
label: 'Delete',
|
||||
icon: <BiTrash
|
||||
size={15}
|
||||
className="translate-x-[-1px]"
|
||||
/>,
|
||||
className: 'text-error *:hover:text-error',
|
||||
action: () => {
|
||||
if (confirm(deleteConfirmationTextForPhoto(photo))) {
|
||||
return deletePhotoAction(
|
||||
photo.id,
|
||||
photo.url,
|
||||
shouldRedirectDelete,
|
||||
).then(() => {
|
||||
revalidatePhoto?.(photo.id, true);
|
||||
registerAdminUpdate?.();
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
return items;
|
||||
}, [
|
||||
photo,
|
||||
includeFavorite,
|
||||
isFav,
|
||||
shouldRedirectFav,
|
||||
revalidatePhoto,
|
||||
shouldRedirectDelete,
|
||||
registerAdminUpdate,
|
||||
]);
|
||||
|
||||
return (
|
||||
isUserSignedIn
|
||||
? <MoreMenu {...{
|
||||
items,
|
||||
...props,
|
||||
}}/>
|
||||
: null
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,131 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { ComponentProps, useMemo } from 'react';
|
||||
import { pathForAdminPhotoEdit, pathForPhoto } from '@/app/paths';
|
||||
import {
|
||||
deletePhotoAction,
|
||||
syncPhotoAction,
|
||||
toggleFavoritePhotoAction,
|
||||
} from '@/photo/actions';
|
||||
import {
|
||||
Photo,
|
||||
deleteConfirmationTextForPhoto,
|
||||
downloadFileNameForPhoto,
|
||||
} from '@/photo';
|
||||
import { isPathFavs, isPhotoFav } from '@/tag';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { BiTrash } from 'react-icons/bi';
|
||||
import MoreMenu from '@/components/more/MoreMenu';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import { RevalidatePhoto } from '@/photo/InfinitePhotoScroll';
|
||||
import { MdOutlineFileDownload } from 'react-icons/md';
|
||||
import MoreMenuItem from '@/components/more/MoreMenuItem';
|
||||
import IconGrSync from '@/components/icons/IconGrSync';
|
||||
import { isPhotoOutdated } from '@/photo/outdated';
|
||||
import InsightsIndicatorDot from './insights/InsightsIndicatorDot';
|
||||
import IconFavs from '@/components/icons/IconFavs';
|
||||
import IconEdit from '@/components/icons/IconEdit';
|
||||
|
||||
export default function AdminPhotoMenuClient({
|
||||
photo,
|
||||
revalidatePhoto,
|
||||
includeFavorite = true,
|
||||
...props
|
||||
}: Omit<ComponentProps<typeof MoreMenu>, 'items'> & {
|
||||
photo: Photo
|
||||
revalidatePhoto?: RevalidatePhoto
|
||||
includeFavorite?: boolean
|
||||
}) {
|
||||
const { isUserSignedIn, registerAdminUpdate } = useAppState();
|
||||
|
||||
const isFav = isPhotoFav(photo);
|
||||
const path = usePathname();
|
||||
const shouldRedirectFav = isPathFavs(path) && isFav;
|
||||
const shouldRedirectDelete = pathForPhoto({ photo: photo.id }) === path;
|
||||
|
||||
const items = useMemo(() => {
|
||||
const items: ComponentProps<typeof MoreMenuItem>[] = [{
|
||||
label: 'Edit',
|
||||
icon: <IconEdit
|
||||
size={15}
|
||||
className="translate-x-[0.5px] translate-y-[-0.5px]"
|
||||
/>,
|
||||
href: pathForAdminPhotoEdit(photo.id),
|
||||
}];
|
||||
if (includeFavorite) {
|
||||
items.push({
|
||||
label: isFav ? 'Unfavorite' : 'Favorite',
|
||||
icon: <IconFavs
|
||||
size={14}
|
||||
className="translate-x-[-1px]"
|
||||
highlight={isFav}
|
||||
/>,
|
||||
action: () => toggleFavoritePhotoAction(
|
||||
photo.id,
|
||||
shouldRedirectFav,
|
||||
).then(() => revalidatePhoto?.(photo.id)),
|
||||
});
|
||||
}
|
||||
items.push({
|
||||
label: 'Download',
|
||||
icon: <MdOutlineFileDownload
|
||||
size={17}
|
||||
className="translate-x-[-1px] translate-y-[-0.5px]"
|
||||
/>,
|
||||
href: photo.url,
|
||||
hrefDownloadName: downloadFileNameForPhoto(photo),
|
||||
});
|
||||
items.push({
|
||||
label: 'Sync',
|
||||
labelComplex: <span className="inline-flex items-center gap-2">
|
||||
<span>Sync</span>
|
||||
{isPhotoOutdated(photo) &&
|
||||
<InsightsIndicatorDot
|
||||
colorOverride="blue"
|
||||
className="translate-y-[1.5px]"
|
||||
/>}
|
||||
</span>,
|
||||
icon: <IconGrSync className="translate-x-[-1px]" />,
|
||||
action: () => syncPhotoAction(photo.id)
|
||||
.then(() => revalidatePhoto?.(photo.id)),
|
||||
});
|
||||
items.push({
|
||||
label: 'Delete',
|
||||
icon: <BiTrash
|
||||
size={15}
|
||||
className="translate-x-[-1px]"
|
||||
/>,
|
||||
className: 'text-error *:hover:text-error',
|
||||
action: () => {
|
||||
if (confirm(deleteConfirmationTextForPhoto(photo))) {
|
||||
return deletePhotoAction(
|
||||
photo.id,
|
||||
photo.url,
|
||||
shouldRedirectDelete,
|
||||
).then(() => {
|
||||
revalidatePhoto?.(photo.id, true);
|
||||
registerAdminUpdate?.();
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
return items;
|
||||
}, [
|
||||
photo,
|
||||
includeFavorite,
|
||||
isFav,
|
||||
shouldRedirectFav,
|
||||
revalidatePhoto,
|
||||
shouldRedirectDelete,
|
||||
registerAdminUpdate,
|
||||
]);
|
||||
|
||||
return (
|
||||
isUserSignedIn
|
||||
? <MoreMenu {...{
|
||||
items,
|
||||
...props,
|
||||
}}/>
|
||||
: null
|
||||
);
|
||||
}
|
||||
@ -46,10 +46,11 @@ export default function MoreMenu({
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button
|
||||
className={clsx(
|
||||
'p-1 min-h-0 border-none shadow-none hover:outline-hidden',
|
||||
'hover:bg-gray-100 active:bg-gray-100',
|
||||
'p-1 min-h-0 border-none shadow-none',
|
||||
'hover:bg-gray-100 active:bg-gray-200/75',
|
||||
'dark:hover:bg-gray-800/75 dark:active:bg-gray-900',
|
||||
'text-dim',
|
||||
'outline-none',
|
||||
buttonClassName,
|
||||
isOpen && buttonClassNameOpen,
|
||||
)}
|
||||
|
||||
@ -30,7 +30,7 @@ import {
|
||||
ALLOW_PUBLIC_DOWNLOADS,
|
||||
SHOW_TAKEN_AT_TIME,
|
||||
} from '@/app/config';
|
||||
import AdminPhotoMenuClient from '@/admin/AdminPhotoMenuClient';
|
||||
import AdminPhotoMenu from '@/admin/AdminPhotoMenu';
|
||||
import { RevalidatePhoto } from './InfinitePhotoScroll';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import useVisible from '@/utility/useVisible';
|
||||
@ -179,7 +179,7 @@ export default function PhotoLarge({
|
||||
? 'w-[80%]'
|
||||
: undefined;
|
||||
|
||||
const largePhotoContent =
|
||||
const renderLargePhoto =
|
||||
<div className={clsx(
|
||||
'relative',
|
||||
arePhotosMatted && 'flex items-center justify-center',
|
||||
@ -228,6 +228,15 @@ export default function PhotoLarge({
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
const renderAdminMenu =
|
||||
<AdminPhotoMenu {...{
|
||||
photo,
|
||||
revalidatePhoto,
|
||||
buttonClassName: 'translate-y-[-4px]',
|
||||
includeFavorite: includeFavoriteInAdminMenu,
|
||||
ariaLabel: `Admin menu for '${titleForPhoto(photo)}' photo`,
|
||||
}} />;
|
||||
|
||||
const largePhotoContainerClassName = clsx(arePhotosMatted &&
|
||||
'flex items-center justify-center aspect-3/2 bg-gray-100',
|
||||
);
|
||||
@ -238,14 +247,14 @@ export default function PhotoLarge({
|
||||
className={className}
|
||||
contentMain={showZoomControls
|
||||
? <div className={largePhotoContainerClassName}>
|
||||
{largePhotoContent}
|
||||
{renderLargePhoto}
|
||||
</div>
|
||||
: <Link
|
||||
href={pathForPhoto({ photo })}
|
||||
className={largePhotoContainerClassName}
|
||||
prefetch={prefetch}
|
||||
>
|
||||
{largePhotoContent}
|
||||
{renderLargePhoto}
|
||||
</Link>}
|
||||
contentSide={
|
||||
<DivDebugBaselineGrid className={clsx(
|
||||
@ -257,13 +266,8 @@ export default function PhotoLarge({
|
||||
)}>
|
||||
{/* Meta */}
|
||||
<div>
|
||||
<div className="float-right translate-y-[-4px]">
|
||||
<AdminPhotoMenuClient {...{
|
||||
photo,
|
||||
revalidatePhoto,
|
||||
includeFavorite: includeFavoriteInAdminMenu,
|
||||
ariaLabel: `Admin menu for '${titleForPhoto(photo)}' photo`,
|
||||
}} />
|
||||
<div className="float-right hidden md:block">
|
||||
{renderAdminMenu}
|
||||
</div>
|
||||
{hasTitle && (showTitleAsH1
|
||||
? <h1>{renderPhotoLink}</h1>
|
||||
@ -320,6 +324,9 @@ export default function PhotoLarge({
|
||||
'space-y-baseline',
|
||||
!hasTitleContent && !hasMetaContent && 'md:-mt-baseline',
|
||||
)}>
|
||||
<div className="float-right md:hidden">
|
||||
{renderAdminMenu}
|
||||
</div>
|
||||
{showExifContent &&
|
||||
<>
|
||||
<ul className="text-medium">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user