diff --git a/src/admin/AdminPhotoMenu.tsx b/src/admin/AdminPhotoMenu.tsx index c32c7c84..84f7259b 100644 --- a/src/admin/AdminPhotoMenu.tsx +++ b/src/admin/AdminPhotoMenu.tsx @@ -1,8 +1,10 @@ import { authCached } from '@/auth/cache'; -import AdminPhotoMenuClient, { AdminPhotoMenuClientProps } - from './AdminPhotoMenuClient'; +import AdminPhotoMenuClient from './AdminPhotoMenuClient'; +import { ComponentProps } from 'react'; -export default async function AdminPhotoMenu(props: AdminPhotoMenuClientProps) { +export default async function AdminPhotoMenu( + props: ComponentProps, +) { const session = await authCached(); return Boolean(session?.user?.email) ? diff --git a/src/admin/AdminPhotoMenuClient.tsx b/src/admin/AdminPhotoMenuClient.tsx index d861dd1f..6f72e2b1 100644 --- a/src/admin/AdminPhotoMenuClient.tsx +++ b/src/admin/AdminPhotoMenuClient.tsx @@ -3,19 +3,42 @@ import { ComponentProps } from 'react'; import { pathForAdminPhotoEdit } from '@/site/paths'; import MoreMenu from '../components/MoreMenu'; - -export interface AdminPhotoMenuClientProps - extends Omit, 'items'> { - photoId: string -} +import { toggleFavoritePhoto } from '@/photo/actions'; +import { FaRegEdit, FaRegStar, FaStar } from 'react-icons/fa'; +import { Photo } from '@/photo'; +import { isPathFavs, isPhotoFav } from '@/tag'; +import { usePathname } from 'next/navigation'; export default function AdminPhotoMenuClient({ - photoId, + photo, ...props -}: AdminPhotoMenuClientProps) { +}: Omit, 'items'> & { + photo: Photo +}) { + const isFav = isPhotoFav(photo); + const path = usePathname(); + const shouldRedirect = isPathFavs(path) && isFav; return ( , + href: pathForAdminPhotoEdit(photo.id), + }, { + label: isFav ? 'Unfavorite' : 'Favorite', + icon: isFav + ? + : , + action: () => toggleFavoritePhoto(photo.id, shouldRedirect), + }, + ], ...props, }}/> ); diff --git a/src/components/Checklist.tsx b/src/components/Checklist.tsx index 811e9b46..cb80dded 100644 --- a/src/components/Checklist.tsx +++ b/src/components/Checklist.tsx @@ -4,10 +4,12 @@ import { clsx } from 'clsx/lite'; export default function Checklist({ title, icon, + optional, children, }: { title: string icon?: ReactNode + optional?: boolean children: ReactNode }) { return ( @@ -15,11 +17,13 @@ export default function Checklist({
{icon} -
- {title} +
+
{title}
+ {optional && +
(Optional)
}
{title} - {optional && ' (optional)'}
{children} diff --git a/src/components/MoreMenu.tsx b/src/components/MoreMenu.tsx index d553d059..8de42a45 100644 --- a/src/components/MoreMenu.tsx +++ b/src/components/MoreMenu.tsx @@ -2,17 +2,46 @@ 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, + }[] className?: string buttonClassName?: string }) { + const [isLoading, setIsLoading] = useState(false); + + const itemClass = clsx( + 'block w-full', + 'border-none min-h-0 bg-transparent', + 'text-left text-main', + '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', + 'shadow-none', + isLoading && 'cursor-not-allowed opacity-50', + ); + + const renderItemContent = ( + label: ReactNode, + icon?: ReactNode, + ) => +
+ {icon} + {label} +
; + return (
- {items.map(({ href, label }) => - - - {label} - + {items.map(({ label, icon, href, action }) => + + <> + {href && + + {renderItemContent(label, icon)} + } + {action && + } + )} diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index 2ce202f1..ecfe79ac 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -89,8 +89,8 @@ export default function PhotoLarge({
-
- +
+
@@ -104,7 +104,7 @@ export default function PhotoLarge({ type="text-only" /> {showSimulation && photo.filmSimulation && -
+
diff --git a/src/photo/actions.ts b/src/photo/actions.ts index db8137ec..0f272b48 100644 --- a/src/photo/actions.ts +++ b/src/photo/actions.ts @@ -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); + } } } diff --git a/src/site/SiteChecklistClient.tsx b/src/site/SiteChecklistClient.tsx index e69b363d..7b08db87 100644 --- a/src/site/SiteChecklistClient.tsx +++ b/src/site/SiteChecklistClient.tsx @@ -29,7 +29,7 @@ export default function SiteChecklistClient({ hasAwsS3Storage, hasMultipleStorageProviders, currentStorage, - hasAuth, + hasAuthSecret, hasAdminUser, hasTitle, hasDomain, @@ -198,26 +198,27 @@ export default function SiteChecklistClient({ > Store auth secret in environment variable: -
- -
- {secret} -
- {renderCopyButton('Secret', secret)} - } - onClick={refreshSecret} - isLoading={isPendingSecret} - spinnerColor="text" - /> + {!hasAuthSecret && +
+ +
+ {secret} +
+ {renderCopyButton('Secret', secret)} + } + onClick={refreshSecret} + isLoading={isPendingSecret} + spinnerColor="text" + /> +
-
- -
+ +
} {renderEnvVars(['AUTH_SECRET'])} } + optional > } + optional > 0, + hasAuthSecret: (process.env.AUTH_SECRET ?? '').length > 0, hasAdminUser: ( (process.env.ADMIN_EMAIL ?? '').length > 0 && (process.env.ADMIN_PASSWORD ?? '').length > 0 @@ -134,5 +134,5 @@ export type ConfigChecklistStatus = typeof CONFIG_CHECKLIST_STATUS; export const IS_SITE_READY = CONFIG_CHECKLIST_STATUS.hasPostgres && CONFIG_CHECKLIST_STATUS.hasStorage && - CONFIG_CHECKLIST_STATUS.hasAuth && + CONFIG_CHECKLIST_STATUS.hasAuthSecret && CONFIG_CHECKLIST_STATUS.hasAdminUser; diff --git a/src/tag/index.ts b/src/tag/index.ts index 58384d0a..60f9ed86 100644 --- a/src/tag/index.ts +++ b/src/tag/index.ts @@ -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;