diff --git a/src/admin/AdminPhotoMenuClient.tsx b/src/admin/AdminPhotoMenuClient.tsx
index 7e86c7e5..c9fc77b8 100644
--- a/src/admin/AdminPhotoMenuClient.tsx
+++ b/src/admin/AdminPhotoMenuClient.tsx
@@ -2,7 +2,11 @@
import { ComponentProps, useMemo } from 'react';
import { pathForAdminPhotoEdit, pathForPhoto } from '@/app/paths';
-import { deletePhotoAction, toggleFavoritePhotoAction } from '@/photo/actions';
+import {
+ deletePhotoAction,
+ syncPhotoAction,
+ toggleFavoritePhotoAction,
+} from '@/photo/actions';
import { FaRegEdit, FaRegStar, FaStar } from 'react-icons/fa';
import {
Photo,
@@ -17,6 +21,7 @@ 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 '@/app/IconGrSync';
export default function AdminPhotoMenuClient({
photo,
@@ -65,17 +70,24 @@ export default function AdminPhotoMenuClient({
label: 'Download',
icon: ,
href: photo.url,
hrefDownloadName: downloadFileNameForPhoto(photo),
});
+ items.push({
+ label: 'Sync',
+ icon: ,
+ action: () => syncPhotoAction(photo.id)
+ .then(() => revalidatePhoto?.(photo.id)),
+ });
items.push({
label: 'Delete',
icon: ,
+ className: 'text-error',
action: () => {
if (confirm(deleteConfirmationTextForPhoto(photo))) {
return deletePhotoAction(
diff --git a/src/app/paths.ts b/src/app/paths.ts
index 22d49cde..5f97cc53 100644
--- a/src/app/paths.ts
+++ b/src/app/paths.ts
@@ -33,6 +33,10 @@ const PATH_CAMERA_DYNAMIC = `${PREFIX_CAMERA}/[make]/[model]`;
const PATH_FILM_SIMULATION_DYNAMIC = `${PREFIX_FILM_SIMULATION}/[simulation]`;
const PATH_FOCAL_LENGTH_DYNAMIC = `${PREFIX_FOCAL_LENGTH}/[focal]`;
+// Search params
+export const SEARCH_PARAM_SHOW = 'show';
+export const SEARCH_PARAM_SHOW_RECIPE = 'recipe';
+
// Admin paths
export const PATH_ADMIN_PHOTOS = `${PATH_ADMIN}/photos`;
export const PATH_ADMIN_OUTDATED = `${PATH_ADMIN}/outdated`;
@@ -78,7 +82,9 @@ export const PATHS_TO_CACHE = [
...PATHS_ADMIN,
];
-type PhotoPathParams = { photo: PhotoOrPhotoId } & PhotoSetCategory;
+type PhotoPathParams = { photo: PhotoOrPhotoId } & PhotoSetCategory & {
+ showRecipe?: boolean
+};
// Absolute paths
export const ABSOLUTE_PATH_FOR_HOME_IMAGE = `${BASE_URL}/home-image`;
@@ -103,6 +109,7 @@ export const pathForPhoto = ({
camera,
simulation,
focal,
+ showRecipe,
}: PhotoPathParams) =>
typeof photo !== 'string' && photo.hidden
? `${pathForTag(TAG_HIDDEN)}/${getPhotoId(photo)}`
@@ -114,7 +121,9 @@ export const pathForPhoto = ({
? `${pathForFilmSimulation(simulation)}/${getPhotoId(photo)}`
: focal
? `${pathForFocalLength(focal)}/${getPhotoId(photo)}`
- : `${PREFIX_PHOTO}/${getPhotoId(photo)}`;
+ : `${PREFIX_PHOTO}/${getPhotoId(photo)}` + (showRecipe
+ ? `?${SEARCH_PARAM_SHOW}=${SEARCH_PARAM_SHOW_RECIPE}`
+ : '');
export const pathForTag = (tag: string) =>
`${PREFIX_TAG}/${tag}`;
diff --git a/src/components/more/MoreMenuItem.tsx b/src/components/more/MoreMenuItem.tsx
index 914f6471..cc2c904d 100644
--- a/src/components/more/MoreMenuItem.tsx
+++ b/src/components/more/MoreMenuItem.tsx
@@ -12,6 +12,7 @@ export default function MoreMenuItem({
icon,
href,
hrefDownloadName,
+ className,
action,
shouldPreventDefault = true,
}: {
@@ -19,6 +20,7 @@ export default function MoreMenuItem({
icon?: ReactNode
href?: string
hrefDownloadName?: string
+ className?: string
action?: () => Promise | void
shouldPreventDefault?: boolean
}) {
@@ -43,6 +45,7 @@ export default function MoreMenuItem({
isLoading
? 'cursor-not-allowed opacity-50'
: 'cursor-pointer',
+ className,
)}
onClick={async e => {
if (shouldPreventDefault) { e.preventDefault(); }
diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx
index 590d7f31..dd950545 100644
--- a/src/photo/PhotoLarge.tsx
+++ b/src/photo/PhotoLarge.tsx
@@ -15,6 +15,8 @@ import Link from 'next/link';
import {
pathForFocalLength,
pathForPhoto,
+ SEARCH_PARAM_SHOW,
+ SEARCH_PARAM_SHOW_RECIPE,
} from '@/app/paths';
import PhotoTags from '@/tag/PhotoTags';
import ShareButton from '@/share/ShareButton';
@@ -43,6 +45,7 @@ import ZoomControls, { ZoomControlsRef } from '@/components/image/ZoomControls';
import PhotoRecipe from './PhotoRecipe';
import { TbChecklist } from 'react-icons/tb';
import { IoCloseSharp } from 'react-icons/io5';
+import { useSearchParams } from 'next/navigation';
export default function PhotoLarge({
photo,
@@ -91,7 +94,10 @@ export default function PhotoLarge({
const zoomControlsRef = useRef(null);
- const [shouldShowRecipe, setShouldShowRecipe] = useState(false);
+ const params = useSearchParams();
+ const showRecipeInitially =
+ params.get(SEARCH_PARAM_SHOW) === SEARCH_PARAM_SHOW_RECIPE;
+ const [shouldShowRecipe, setShouldShowRecipe] = useState(showRecipeInitially);
const recipeButtonRef = useRef(null);
const {
@@ -333,9 +339,8 @@ export default function PhotoLarge({
// Prevent collision with admin button
!hasNonDateContent && isUserSignedIn && 'md:pr-7',
)}
- // 'createdAt' is a naive datetime which
- // does not require a timezone and will not
- // cause server/client time mismatches
+ // 'createdAt' is a naive datetime which does not require
+ // a timezone and will not cause server/client mismatch
timezone={null}
hideTime={!SHOW_TAKEN_AT_TIME}
/>