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,
};
}