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} />