Add favorites to admin photo menu

This commit is contained in:
Sam Becker 2024-02-10 01:11:23 -06:00
parent 8151a4f1cd
commit d860777604
5 changed files with 100 additions and 24 deletions

View File

@ -3,16 +3,38 @@
import { ComponentProps } from 'react';
import { pathForAdminPhotoEdit } from '@/site/paths';
import MoreMenu from '../components/MoreMenu';
import { toggleFavoritePhoto } from '@/photo/actions';
import { FaRegEdit, FaStar } from 'react-icons/fa';
import { Photo } from '@/photo';
import { isPathFavs, isPhotoFav } from '@/tag';
import clsx from 'clsx/lite';
import { usePathname } from 'next/navigation';
export default function AdminPhotoMenuClient({
photoId,
photo,
...props
}: Omit<ComponentProps<typeof MoreMenu>, 'items'> & {
photoId: string
photo: Photo
}) {
const isFav = isPhotoFav(photo);
const path = usePathname();
const shouldRedirect = isPathFavs(path) && isFav;
return (
<MoreMenu {...{
items: [{ href: pathForAdminPhotoEdit(photoId), label: 'Edit Photo' }],
items: [
{
label: 'Edit Photo',
icon: <FaRegEdit size={14} className="translate-y-[-0.5px]" />,
href: pathForAdminPhotoEdit(photo.id),
}, {
label: isFav ? 'Unfavorite' : 'Favorite',
icon: <FaStar stroke='text-amber-500' className={clsx(
'translate-x-[-1px]',
isFav && 'text-amber-500',
)} />,
action: () => toggleFavoritePhoto(photo.id, shouldRedirect),
},
],
...props,
}}/>
);

View File

@ -2,17 +2,45 @@ import { clsx} from 'clsx/lite';
import Link from 'next/link';
import { Menu } from '@headlessui/react';
import { FiMoreHorizontal } from 'react-icons/fi';
import { ReactNode } from 'react';
import { Fragment, ReactNode, useState } from 'react';
export default function MoreMenu({
items,
className,
buttonClassName,
}: {
items: { href: string, label: ReactNode }[]
items: {
label: ReactNode,
icon?: ReactNode,
href?: string,
action?: () => Promise<void>,
}[]
className?: string
buttonClassName?: string
}) {
const [isLoading, setIsLoading] = useState(false);
const itemClass = clsx(
'block w-full',
'border-none min-h-0 bg-transparent',
'text-left',
'px-3 py-1.5 rounded-[3px]',
'hover:text-main',
'hover:bg-gray-50 active:bg-gray-100',
'hover:dark:bg-gray-900/75 active:dark:bg-gray-900',
'whitespace-nowrap',
isLoading && 'cursor-not-allowed opacity-50',
);
const renderItemContent = (
label: ReactNode,
icon?: ReactNode,
) =>
<div className="flex items-center">
<span className="w-6">{icon}</span>
<span>{label}</span>
</div>;
return (
<div className={clsx(
className,
@ -30,27 +58,38 @@ export default function MoreMenu({
<Menu.Items className={clsx(
'block outline-none h-auto',
'absolute top-6',
'text-left',
'md:right-1',
'text-sm',
'p-1 rounded-md border',
'bg-content',
'shadow-lg dark:shadow-xl',
)}>
{items.map(({ href, label }) =>
<Menu.Item key={href}>
<Link
href={href}
className={clsx(
'block',
'px-3 py-1.5 rounded-[3px]',
'hover:text-main',
'hover:bg-gray-50 active:bg-gray-100',
'hover:dark:bg-gray-900/75 active:dark:bg-gray-900',
'whitespace-nowrap',
)}
>
{label}
</Link>
{items.map(({ label, icon, href, action }) =>
<Menu.Item
key={`${label}`}
disabled={isLoading}
as={Fragment}
>
<>
{href &&
<Link
href={href}
className={itemClass}
>
{renderItemContent(label, icon)}
</Link>}
{action &&
<button
onClick={() => {
setIsLoading(true);
action().finally(() => setIsLoading(false));
}}
className={itemClass}
>
{renderItemContent(label, icon)}
</button>}
</>
</Menu.Item>
)}
</Menu.Items>

View File

@ -90,7 +90,7 @@ export default function PhotoLarge({
</div>
<Suspense>
<div className="h-4 translate-y-[-3.5px] z-10">
<AdminPhotoMenu photoId={photo.id} />
<AdminPhotoMenu photo={photo} />
</div>
</Suspense>
</div>

View File

@ -23,7 +23,7 @@ import {
revalidateAllKeysAndPaths,
revalidatePhotosKey,
} from '@/photo/cache';
import { PATH_ADMIN_PHOTOS, PATH_ADMIN_TAGS } from '@/site/paths';
import { PATH_ADMIN_PHOTOS, PATH_ADMIN_TAGS, PATH_ROOT } from '@/site/paths';
import { extractExifDataFromBlobPath } from './server';
import { TAG_FAVS, isTagFavs } from '@/tag';
import { convertPhotoToPhotoDbInsert } from '.';
@ -52,7 +52,10 @@ export async function updatePhotoAction(formData: FormData) {
redirect(PATH_ADMIN_PHOTOS);
}
export async function toggleFavoritePhoto(photoId: string) {
export async function toggleFavoritePhoto(
photoId: string,
shouldRedirect?: boolean,
) {
const photo = await getPhoto(photoId);
if (photo) {
const { tags } = photo;
@ -61,6 +64,9 @@ export async function toggleFavoritePhoto(photoId: string) {
: [...tags, TAG_FAVS];
await sqlUpdatePhoto(convertPhotoToPhotoDbInsert(photo));
revalidateAllKeysAndPaths();
if (shouldRedirect) {
redirect(PATH_ROOT);
}
}
}

View File

@ -4,7 +4,11 @@ import {
descriptionForPhotoSet,
photoQuantityText,
} from '@/photo';
import { absolutePathForTag, absolutePathForTagImage } from '@/site/paths';
import {
absolutePathForTag,
absolutePathForTagImage,
getPathComponents,
} from '@/site/paths';
import { capitalizeWords, convertStringToArray } from '@/utility/string';
export const TAG_FAVS = 'favs';
@ -77,3 +81,8 @@ export const generateMetaForTag = (
});
export const isTagFavs = (tag: string) => tag.toLowerCase() === TAG_FAVS;
export const isPhotoFav = ({ tags }: Photo) => tags.some(isTagFavs);
export const isPathFavs = (pathname?: string) =>
getPathComponents(pathname).tag === TAG_FAVS;