diff --git a/app/film/[simulation]/image/route.tsx b/app/film/[simulation]/image/route.tsx index 386c5fc0..2cb562fa 100644 --- a/app/film/[simulation]/image/route.tsx +++ b/app/film/[simulation]/image/route.tsx @@ -1,7 +1,7 @@ import { getPhotosCached } from '@/photo/cache'; import { IMAGE_OG_DIMENSION_SMALL, - MAX_PHOTOS_TO_SHOW_PER_TAG, + MAX_PHOTOS_TO_SHOW_PER_CATEGORY, } from '@/image-response'; import FilmSimulationImageResponse from '@/image-response/FilmSimulationImageResponse'; @@ -39,7 +39,7 @@ export async function GET( { fontFamily, fonts }, headers, ] = await Promise.all([ - getPhotosCached({ limit: MAX_PHOTOS_TO_SHOW_PER_TAG, simulation }), + getPhotosCached({ limit: MAX_PHOTOS_TO_SHOW_PER_CATEGORY, simulation }), getIBMPlexMonoMedium(), getImageResponseCacheControlHeaders(), ]); diff --git a/app/focal/[focal]/image/route.tsx b/app/focal/[focal]/image/route.tsx index 734e5a9a..0254fd99 100644 --- a/app/focal/[focal]/image/route.tsx +++ b/app/focal/[focal]/image/route.tsx @@ -1,7 +1,7 @@ import { getPhotosCached } from '@/photo/cache'; import { IMAGE_OG_DIMENSION_SMALL, - MAX_PHOTOS_TO_SHOW_PER_TAG, + MAX_PHOTOS_TO_SHOW_PER_CATEGORY, } from '@/image-response'; import { getIBMPlexMonoMedium } from '@/app/font'; import { ImageResponse } from 'next/og'; @@ -41,7 +41,7 @@ export async function GET( { fontFamily, fonts }, headers, ] = await Promise.all([ - getPhotosCached({ limit: MAX_PHOTOS_TO_SHOW_PER_TAG, focal }), + getPhotosCached({ limit: MAX_PHOTOS_TO_SHOW_PER_CATEGORY, focal }), getIBMPlexMonoMedium(), getImageResponseCacheControlHeaders(), ]); diff --git a/app/recipe/[recipe]/[photoId]/page.tsx b/app/recipe/[recipe]/[photoId]/page.tsx index c1693527..85d60dce 100644 --- a/app/recipe/[recipe]/[photoId]/page.tsx +++ b/app/recipe/[recipe]/[photoId]/page.tsx @@ -62,7 +62,7 @@ export async function generateMetadata({ }; } -export default async function PhotoTagPage({ +export default async function PhotoRecipePage({ params, }: PhotoRecipeProps) { const { photoId, recipe: recipeFromParams } = await params; diff --git a/app/recipe/[recipe]/image/route.tsx b/app/recipe/[recipe]/image/route.tsx index 56e999e5..619712af 100644 --- a/app/recipe/[recipe]/image/route.tsx +++ b/app/recipe/[recipe]/image/route.tsx @@ -1,7 +1,7 @@ import { getPhotosCached } from '@/photo/cache'; import { IMAGE_OG_DIMENSION_SMALL, - MAX_PHOTOS_TO_SHOW_PER_TAG, + MAX_PHOTOS_TO_SHOW_PER_CATEGORY, } from '@/image-response'; import { getIBMPlexMonoMedium } from '@/app/font'; import { ImageResponse } from 'next/og'; @@ -37,7 +37,7 @@ export async function GET( { fontFamily, fonts }, headers, ] = await Promise.all([ - getPhotosCached({ limit: MAX_PHOTOS_TO_SHOW_PER_TAG, recipe }), + getPhotosCached({ recipe, limit: MAX_PHOTOS_TO_SHOW_PER_CATEGORY }), getIBMPlexMonoMedium(), getImageResponseCacheControlHeaders(), ]); diff --git a/app/shot-on/[make]/[model]/image/route.tsx b/app/shot-on/[make]/[model]/image/route.tsx index e30c5e94..30d7a848 100644 --- a/app/shot-on/[make]/[model]/image/route.tsx +++ b/app/shot-on/[make]/[model]/image/route.tsx @@ -2,7 +2,7 @@ import { getPhotosCached } from '@/photo/cache'; import { Camera, CameraProps, getCameraFromParams } from '@/camera'; import { IMAGE_OG_DIMENSION_SMALL, - MAX_PHOTOS_TO_SHOW_PER_TAG, + MAX_PHOTOS_TO_SHOW_PER_CATEGORY, } from '@/image-response'; import CameraImageResponse from '@/image-response/CameraImageResponse'; import { getIBMPlexMonoMedium } from '@/app/font'; @@ -39,7 +39,7 @@ export async function GET( headers, ] = await Promise.all([ getPhotosCached({ - limit: MAX_PHOTOS_TO_SHOW_PER_TAG, + limit: MAX_PHOTOS_TO_SHOW_PER_CATEGORY, camera: camera, }), getIBMPlexMonoMedium(), diff --git a/app/tag/[tag]/image/route.tsx b/app/tag/[tag]/image/route.tsx index 6bb84066..7ca795eb 100644 --- a/app/tag/[tag]/image/route.tsx +++ b/app/tag/[tag]/image/route.tsx @@ -1,7 +1,7 @@ import { getPhotosCached } from '@/photo/cache'; import { IMAGE_OG_DIMENSION_SMALL, - MAX_PHOTOS_TO_SHOW_PER_TAG, + MAX_PHOTOS_TO_SHOW_PER_CATEGORY, } from '@/image-response'; import TagImageResponse from '@/image-response/TagImageResponse'; import { getIBMPlexMonoMedium } from '@/app/font'; @@ -37,7 +37,7 @@ export async function GET( { fontFamily, fonts }, headers, ] = await Promise.all([ - getPhotosCached({ limit: MAX_PHOTOS_TO_SHOW_PER_TAG, tag }), + getPhotosCached({ limit: MAX_PHOTOS_TO_SHOW_PER_CATEGORY, tag }), getIBMPlexMonoMedium(), getImageResponseCacheControlHeaders(), ]); diff --git a/src/image-response/components/ImagePhotoGrid.tsx b/src/image-response/components/ImagePhotoGrid.tsx index fa2c8793..1d2d1c0e 100644 --- a/src/image-response/components/ImagePhotoGrid.tsx +++ b/src/image-response/components/ImagePhotoGrid.tsx @@ -14,11 +14,13 @@ export default function ImagePhotoGrid({ height, imagePosition = 'center', gap = 4, + imageStyle, }: ({ photos: Photo[] height: number imagePosition?: 'center' | 'top' gap?: number + imageStyle?: React.CSSProperties } & ( { width: NextImageSize, widthArbitrary?: undefined } | { width?: undefined, widthArbitrary: number } @@ -46,13 +48,15 @@ export default function ImagePhotoGrid({ (rows - 1) * gap / rows; return ( -
+
{photos.slice(0, count).map(({ id, url }) =>
(null); const refTriggers = useMemo(() => [refRecipeButton], []); const { - shouldShowRecipe, - toggleRecipe, - hideRecipe, - } = useRecipeState({ + shouldShowRecipeOverlay, + toggleRecipeOverlay, + hideRecipeOverlay, + } = useRecipeOverlay({ ref: refRecipe, refTriggers, }); @@ -193,9 +193,12 @@ export default function PhotoLarge({
- {(shouldShowRecipe || shouldDebugRecipeOverlays) && + {(shouldShowRecipeOverlay || shouldDebugRecipeOverlays) && photo.recipeData && photo.filmSimulation && }
@@ -214,6 +217,17 @@ export default function PhotoLarge({ 'flex items-center justify-center aspect-3/2 bg-gray-100', ); + const shouldRenderSimulation = ( + SHOW_FILM_SIMULATIONS && + showSimulation && + photo.filmSimulation + ); + + const shouldRenderRecipe = ( + SHOW_RECIPES && + photo.recipeData + ); + return ( {photo.isoFormatted}
  • {photo.exposureCompensationFormatted ?? '0ev'}
  • - {( - ( - SHOW_FILM_SIMULATIONS && - showSimulation && - photo.filmSimulation - ) || - (SHOW_RECIPES && photo.recipeData) - ) && + {(shouldRenderSimulation || shouldRenderRecipe) &&
    - {( - SHOW_FILM_SIMULATIONS && - showSimulation && - photo.filmSimulation - ) && + {shouldRenderSimulation && photo.filmSimulation && } - {SHOW_RECIPES && photo.recipeData && + {shouldRenderRecipe && - {shouldShowRecipe + {shouldShowRecipeOverlay ? : value < 0 ? value : `+${value}`; +import { addSign, formatWhiteBalance } from '.'; export default function PhotoRecipeOGTile({ - recipe: { + recipe, + simulation, + iso, + exposure, +}: { + ref?: RefObject + recipe: FujifilmRecipe + simulation: FilmSimulation + iso?: string + exposure?: string +}) { + const { dynamicRange, whiteBalance, highISONoiseReduction, @@ -24,24 +32,9 @@ export default function PhotoRecipeOGTile({ grainEffect, bwAdjustment, bwMagentaGreen, - }, - simulation, - iso, - exposure, -}: { - ref?: RefObject - recipe: FujifilmRecipe - simulation: FilmSimulation - iso?: string - exposure?: string - onClose?: () => void -}) { - const whiteBalanceTypeFormatted = - whiteBalance.type === 'kelvin' && whiteBalance.colorTemperature - ? `${whiteBalance.colorTemperature}K` - : whiteBalance.type - .replace(/auto./i, '') - .replaceAll('-', ' '); + } = recipe; + + const whiteBalanceTypeFormatted = formatWhiteBalance(recipe); const renderRow = (children: ReactNode, className?: string) =>
    -
    +
    {typeof value === 'number' ? addSign(value) : value}
    {label &&
    value < 0 ? value : `+${value}`; +import { addSign, formatWhiteBalance, RecipeProps } from '.'; export default function PhotoRecipeOverlay({ ref, - recipe: { + recipe, + simulation, + iso, + exposure, + onClose, +}: RecipeProps & { + ref?: RefObject + onClose?: () => void +}) { + const { dynamicRange, whiteBalance, highISONoiseReduction, @@ -27,21 +34,9 @@ export default function PhotoRecipeOverlay({ grainEffect, bwAdjustment, bwMagentaGreen, - }, - simulation, - iso, - exposure, - onClose, -}: RecipeProps & { - ref?: RefObject - onClose?: () => void -}) { - const whiteBalanceTypeFormatted = - whiteBalance.type === 'kelvin' && whiteBalance.colorTemperature - ? `${whiteBalance.colorTemperature}K` - : whiteBalance.type - .replace(/auto./i, '') - .replaceAll('-', ' '); + } = recipe; + + const whiteBalanceTypeFormatted = formatWhiteBalance(recipe); const renderRow = (children: ReactNode) =>
    {children}
    ; diff --git a/src/recipe/RecipeHeader.tsx b/src/recipe/RecipeHeader.tsx index 422f5fca..bca9a67f 100644 --- a/src/recipe/RecipeHeader.tsx +++ b/src/recipe/RecipeHeader.tsx @@ -4,7 +4,7 @@ import { Photo, PhotoDateRange } from '@/photo'; import PhotoHeader from '@/photo/PhotoHeader'; import PhotoRecipe from './PhotoRecipe'; import { useAppState } from '@/state/AppState'; -import { descriptionForRecipePhotos, photoHasRecipe } from '.'; +import { descriptionForRecipePhotos, getPhotoWithRecipeFromPhotos } from '.'; export default function RecipeHeader({ recipe, photos, @@ -22,9 +22,7 @@ export default function RecipeHeader({ }) { const { setRecipeModalProps } = useAppState(); - const photo = photoHasRecipe(selectedPhoto) - ? selectedPhoto - : photos.find(photoHasRecipe); + const photo = getPhotoWithRecipeFromPhotos(photos, selectedPhoto); return ( +const photoHasRecipe = (photo?: Photo) => photo?.filmSimulation && photo?.recipeData; +export const getPhotoWithRecipeFromPhotos = ( + photos: Photo[], + preferredPhoto?: Photo, +) => + photoHasRecipe(preferredPhoto) + ? preferredPhoto + : photos.find(photoHasRecipe); + export const sortRecipesWithCount = (recipes: Recipes = []) => recipes.sort((a, b) => a.recipe.localeCompare(b.recipe)); @@ -78,3 +86,12 @@ export const convertRecipesForForm = (recipes: Recipes = []) => annotation: formatCount(count), annotationAria: formatCountDescriptive(count), })); + +export const addSign = (value = 0) => value < 0 ? value : `+${value}`; + +export const formatWhiteBalance = ({ whiteBalance }: FujifilmRecipe) => + whiteBalance.type === 'kelvin' && whiteBalance.colorTemperature + ? `${whiteBalance.colorTemperature}K` + : whiteBalance.type + .replace(/auto./i, '') + .replaceAll('-', ' '); diff --git a/src/recipe/useRecipeState.ts b/src/recipe/useRecipeOverlay.ts similarity index 64% rename from src/recipe/useRecipeState.ts rename to src/recipe/useRecipeOverlay.ts index 0ac10b25..e4e542e3 100644 --- a/src/recipe/useRecipeState.ts +++ b/src/recipe/useRecipeOverlay.ts @@ -7,7 +7,7 @@ import { RefObject, useCallback, useEffect, useState } from 'react'; import { isElementEntirelyInViewport } from '@/utility/dom'; import useClickInsideOutside from '@/utility/useClickInsideOutside'; -export default function useRecipeState({ +export default function useRecipeOverlay({ ref, refTriggers = [], }: { @@ -21,11 +21,11 @@ export default function useRecipeState({ ...pathComponents } = getPathComponents(pathname); - const [shouldShowRecipe, setShouldShowRecipe] = useState(false); + const [shouldShowRecipeOverlay, setShouldShowRecipeOverlay] = useState(false); const setVisibility = useCallback((shouldShow: boolean) => { if (shouldShow) { - setShouldShowRecipe(true); + setShouldShowRecipeOverlay(true); // Only add query param for photo details if (photoId) { window.history.pushState( @@ -39,7 +39,7 @@ export default function useRecipeState({ ); } } else { - setShouldShowRecipe(false); + setShouldShowRecipeOverlay(false); // Only remove query param for photo details if (photoId) { window.history.pushState( @@ -54,27 +54,29 @@ export default function useRecipeState({ } }, [pathComponents, photoId]); - const showRecipe = useCallback(() => setVisibility(true), [setVisibility]); - const hideRecipe = useCallback(() => setVisibility(false), [setVisibility]); - const toggleRecipe = useCallback(() => - setVisibility(!shouldShowRecipe), - [setVisibility, shouldShowRecipe]); + const showRecipeOverlay = + useCallback(() => setVisibility(true), [setVisibility]); + const hideRecipeOverlay = + useCallback(() => setVisibility(false), [setVisibility]); + const toggleRecipeOverlay = useCallback(() => + setVisibility(!shouldShowRecipeOverlay), + [setVisibility, shouldShowRecipeOverlay]); useClickInsideOutside({ htmlElements: [ref, ...refTriggers], - onClickOutside: hideRecipe, + onClickOutside: hideRecipeOverlay, }); useEffect(() => { - if (shouldShowRecipe && !isElementEntirelyInViewport(ref?.current)) { + if (shouldShowRecipeOverlay && !isElementEntirelyInViewport(ref?.current)) { ref?.current?.scrollIntoView({ behavior: 'smooth' }); } - }, [ref, shouldShowRecipe]); + }, [ref, shouldShowRecipeOverlay]); return { - shouldShowRecipe, - showRecipe, - hideRecipe, - toggleRecipe, + shouldShowRecipeOverlay, + showRecipeOverlay, + hideRecipeOverlay, + toggleRecipeOverlay, }; }