Refactor recipe schema and pages
This commit is contained in:
parent
5d6e00559f
commit
1d20cb58b2
@ -1,28 +0,0 @@
|
||||
import SiteGrid from '@/components/SiteGrid';
|
||||
import { getPhoto, getPhotos } from '@/photo/db/query';
|
||||
import PhotoRecipeOverlay from '@/recipe/PhotoRecipeOverlay';
|
||||
|
||||
export default async function AdminRecipePage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ photoId: string }>
|
||||
}) {
|
||||
const { photoId } = await params;
|
||||
const photo = await getPhoto(photoId);
|
||||
const photosHidden = await getPhotos({ hidden: 'only' });
|
||||
const { filmSimulation } = photo!;
|
||||
const { fujifilmRecipe } = photosHidden[0];
|
||||
|
||||
return (
|
||||
<SiteGrid
|
||||
contentMain={photo && fujifilmRecipe && filmSimulation
|
||||
? <PhotoRecipeOverlay
|
||||
photos={[photo]}
|
||||
recipe={fujifilmRecipe}
|
||||
/>
|
||||
: <div>
|
||||
Can't find photo/recipe
|
||||
</div>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
import { getPhoto, getPhotos } from '@/photo/db/query';
|
||||
import PhotoRecipeOverlay from '@/recipe/PhotoRecipeOverlay';
|
||||
|
||||
export default async function AdminRecipePage() {
|
||||
const [
|
||||
photos,
|
||||
photo1,
|
||||
photo2,
|
||||
photo3,
|
||||
photo4,
|
||||
photosHidden,
|
||||
] = await Promise.all([
|
||||
getPhotos({ tag: 'favs' }),
|
||||
getPhoto('4zT6dgPr'),
|
||||
getPhoto('9MopluBn'),
|
||||
getPhoto('ifv8zq45'),
|
||||
getPhoto('2BO2YoW6'),
|
||||
getPhotos({ hidden: 'only', limit: 1 }),
|
||||
]);
|
||||
const { fujifilmRecipe } = photosHidden[0];
|
||||
return (
|
||||
<PhotoRecipeOverlay
|
||||
photos={[
|
||||
...photos,
|
||||
photo1!,
|
||||
photo2!,
|
||||
photo3!,
|
||||
photo4!,
|
||||
]}
|
||||
recipe={fujifilmRecipe!}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -7,6 +7,8 @@ import { getPhotosFilmSimulationDataCached } from '@/simulation/data';
|
||||
import { STATICALLY_OPTIMIZED_PHOTO_CATEGORIES } from '@/app/config';
|
||||
import { Metadata } from 'next/types';
|
||||
import { cache } from 'react';
|
||||
import { PATH_ROOT } from '@/app/paths';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
const getPhotosFilmSimulationDataCachedCached =
|
||||
cache(getPhotosFilmSimulationDataCached);
|
||||
@ -38,6 +40,8 @@ export async function generateMetadata({
|
||||
limit: INFINITE_SCROLL_GRID_INITIAL,
|
||||
});
|
||||
|
||||
if (photos.length === 0) { return {}; }
|
||||
|
||||
const {
|
||||
url,
|
||||
title,
|
||||
@ -75,6 +79,8 @@ export default async function FilmSimulationPage({
|
||||
limit: INFINITE_SCROLL_GRID_INITIAL,
|
||||
});
|
||||
|
||||
if (photos.length === 0) { redirect(PATH_ROOT); }
|
||||
|
||||
return (
|
||||
<FilmSimulationOverview {...{
|
||||
simulation,
|
||||
|
||||
90
app/recipe/[recipe]/[photoId]/page.tsx
Normal file
90
app/recipe/[recipe]/[photoId]/page.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import {
|
||||
RELATED_GRID_PHOTOS_TO_SHOW,
|
||||
descriptionForPhoto,
|
||||
titleForPhoto,
|
||||
} from '@/photo';
|
||||
import { Metadata } from 'next/types';
|
||||
import { redirect } from 'next/navigation';
|
||||
import {
|
||||
PATH_ROOT,
|
||||
absolutePathForPhoto,
|
||||
absolutePathForPhotoImage,
|
||||
} from '@/app/paths';
|
||||
import PhotoDetailPage from '@/photo/PhotoDetailPage';
|
||||
import { getPhotosNearIdCached } from '@/photo/cache';
|
||||
import { cache } from 'react';
|
||||
import { getPhotosMeta } from '@/photo/db/query';
|
||||
|
||||
const getPhotosNearIdCachedCached = cache((
|
||||
photoId: string,
|
||||
recipe: string,
|
||||
) =>
|
||||
getPhotosNearIdCached(
|
||||
photoId,
|
||||
{ recipe, limit: RELATED_GRID_PHOTOS_TO_SHOW + 2 },
|
||||
));
|
||||
|
||||
interface PhotoRecipeProps {
|
||||
params: Promise<{ photoId: string, recipe: string }>
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: PhotoRecipeProps): Promise<Metadata> {
|
||||
const { photoId, recipe: recipeFromParams } = await params;
|
||||
|
||||
const recipe = decodeURIComponent(recipeFromParams);
|
||||
|
||||
const { photo } = await getPhotosNearIdCachedCached(photoId, recipe);
|
||||
|
||||
if (!photo) { return {}; }
|
||||
|
||||
const title = titleForPhoto(photo);
|
||||
const description = descriptionForPhoto(photo);
|
||||
const images = absolutePathForPhotoImage(photo);
|
||||
const url = absolutePathForPhoto({ photo, recipe });
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
images,
|
||||
description,
|
||||
url,
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
description,
|
||||
images,
|
||||
card: 'summary_large_image',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function PhotoTagPage({
|
||||
params,
|
||||
}: PhotoRecipeProps) {
|
||||
const { photoId, recipe: recipeFromParams } = await params;
|
||||
|
||||
const recipe = decodeURIComponent(recipeFromParams);
|
||||
|
||||
const { photo, photos, photosGrid, indexNumber } =
|
||||
await getPhotosNearIdCachedCached(photoId, recipe);
|
||||
|
||||
if (!photo) { redirect(PATH_ROOT); }
|
||||
|
||||
const { count, dateRange } = await getPhotosMeta({ recipe });
|
||||
|
||||
return (
|
||||
<PhotoDetailPage {...{
|
||||
photo,
|
||||
photos,
|
||||
photosGrid,
|
||||
recipe,
|
||||
indexNumber,
|
||||
count,
|
||||
dateRange,
|
||||
}} />
|
||||
);
|
||||
}
|
||||
57
app/recipe/[recipe]/image/route.tsx
Normal file
57
app/recipe/[recipe]/image/route.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { getPhotosCached } from '@/photo/cache';
|
||||
import {
|
||||
IMAGE_OG_DIMENSION_SMALL,
|
||||
MAX_PHOTOS_TO_SHOW_PER_TAG,
|
||||
} from '@/image-response';
|
||||
import { getIBMPlexMonoMedium } from '@/app/font';
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { getImageResponseCacheControlHeaders } from '@/image-response/cache';
|
||||
import { GENERATE_STATIC_PARAMS_LIMIT } from '@/photo/db';
|
||||
import { getUniqueRecipes } from '@/photo/db/query';
|
||||
import {
|
||||
STATICALLY_OPTIMIZED_PHOTO_CATEGORY_OG_IMAGES,
|
||||
IS_PRODUCTION,
|
||||
} from '@/app/config';
|
||||
import RecipeImageResponse from '@/image-response/RecipeImageResponse';
|
||||
|
||||
export let generateStaticParams:
|
||||
(() => Promise<{ recipe: string }[]>) | undefined = undefined;
|
||||
|
||||
if (STATICALLY_OPTIMIZED_PHOTO_CATEGORY_OG_IMAGES && IS_PRODUCTION) {
|
||||
generateStaticParams = async () => {
|
||||
const recipes = await getUniqueRecipes();
|
||||
return recipes
|
||||
.slice(0, GENERATE_STATIC_PARAMS_LIMIT)
|
||||
.map(({ recipe }) => ({ recipe }));
|
||||
};
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
_: Request,
|
||||
context: { params: Promise<{ recipe: string }> },
|
||||
) {
|
||||
const { recipe } = await context.params;
|
||||
|
||||
const [
|
||||
photos,
|
||||
{ fontFamily, fonts },
|
||||
headers,
|
||||
] = await Promise.all([
|
||||
getPhotosCached({ limit: MAX_PHOTOS_TO_SHOW_PER_TAG, recipe }),
|
||||
getIBMPlexMonoMedium(),
|
||||
getImageResponseCacheControlHeaders(),
|
||||
]);
|
||||
|
||||
const { width, height } = IMAGE_OG_DIMENSION_SMALL;
|
||||
|
||||
return new ImageResponse(
|
||||
<RecipeImageResponse {...{
|
||||
recipe,
|
||||
photos,
|
||||
width,
|
||||
height,
|
||||
fontFamily,
|
||||
}}/>,
|
||||
{ width, height, fonts, headers },
|
||||
);
|
||||
}
|
||||
@ -1,27 +1,24 @@
|
||||
import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo';
|
||||
import { getUniqueTags } from '@/photo/db/query';
|
||||
import { getUniqueRecipes } from '@/photo/db/query';
|
||||
import { IS_PRODUCTION } from '@/app/config';
|
||||
import { STATICALLY_OPTIMIZED_PHOTO_CATEGORIES } from '@/app/config';
|
||||
import { PATH_ROOT } from '@/app/paths';
|
||||
import { getPhotosTagDataCached } from '@/tag/data';
|
||||
import type { Metadata } from 'next';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { cache } from 'react';
|
||||
import { convertRecipeToTag, generateMetaForRecipe } from '@/recipe';
|
||||
import { generateMetaForRecipe } from '@/recipe';
|
||||
import RecipeOverview from '@/recipe/RecipeOverview';
|
||||
import { getPhotosRecipeDataCached } from '@/recipe/data';
|
||||
|
||||
const getPhotosTagDataCachedCached = cache((tag: string) =>
|
||||
getPhotosTagDataCached({ tag, limit: INFINITE_SCROLL_GRID_INITIAL}));
|
||||
const getPhotosRecipeDataCachedCached = cache(getPhotosRecipeDataCached);
|
||||
|
||||
export let generateStaticParams:
|
||||
(() => Promise<{ recipe: string }[]>) | undefined = undefined;
|
||||
|
||||
if (STATICALLY_OPTIMIZED_PHOTO_CATEGORIES && IS_PRODUCTION) {
|
||||
generateStaticParams = async () => {
|
||||
const tags = await getUniqueTags();
|
||||
return tags
|
||||
.filter(({ tag }) => tag.startsWith('recipe'))
|
||||
.map(({ tag }) => ({ recipe: tag.replace(/^recipe-?/i, '')}));
|
||||
const recipes = await getUniqueRecipes();
|
||||
return recipes.map(({ recipe }) => ({ recipe }));
|
||||
};
|
||||
}
|
||||
|
||||
@ -39,7 +36,10 @@ export async function generateMetadata({
|
||||
const [
|
||||
photos,
|
||||
{ count, dateRange },
|
||||
] = await getPhotosTagDataCachedCached(convertRecipeToTag(recipe));
|
||||
] = await getPhotosRecipeDataCachedCached({
|
||||
recipe,
|
||||
limit: INFINITE_SCROLL_GRID_INITIAL,
|
||||
});
|
||||
|
||||
if (photos.length === 0) { return {}; }
|
||||
|
||||
@ -77,7 +77,10 @@ export default async function RecipePage({
|
||||
const [
|
||||
photos,
|
||||
{ count, dateRange },
|
||||
] = await getPhotosTagDataCachedCached(convertRecipeToTag(recipe));
|
||||
] = await getPhotosRecipeDataCachedCached({
|
||||
recipe,
|
||||
limit: INFINITE_SCROLL_GRID_INITIAL,
|
||||
});
|
||||
|
||||
if (photos.length === 0) { redirect(PATH_ROOT); }
|
||||
|
||||
|
||||
@ -35,10 +35,6 @@ const PATH_FILM_SIMULATION_DYNAMIC = `${PREFIX_FILM_SIMULATION}/[simulation]`
|
||||
const PATH_FOCAL_LENGTH_DYNAMIC = `${PREFIX_FOCAL_LENGTH}/[focal]`;
|
||||
const PATH_RECIPE_DYNAMIC = `${PREFIX_RECIPE}/[recipe]`;
|
||||
|
||||
// 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`;
|
||||
@ -114,9 +110,8 @@ export const pathForPhoto = ({
|
||||
simulation,
|
||||
focal,
|
||||
recipe,
|
||||
showRecipe,
|
||||
}: PhotoPathParams) => {
|
||||
const path = typeof photo !== 'string' && photo.hidden
|
||||
}: PhotoPathParams) =>
|
||||
typeof photo !== 'string' && photo.hidden
|
||||
? `${pathForTag(TAG_HIDDEN)}/${getPhotoId(photo)}`
|
||||
: tag
|
||||
? `${pathForTag(tag)}/${getPhotoId(photo)}`
|
||||
@ -129,10 +124,6 @@ export const pathForPhoto = ({
|
||||
: recipe
|
||||
? `${pathForRecipe(recipe)}/${getPhotoId(photo)}`
|
||||
: `${PREFIX_PHOTO}/${getPhotoId(photo)}`;
|
||||
return showRecipe
|
||||
? `${path}?${SEARCH_PARAM_SHOW}=${SEARCH_PARAM_SHOW_RECIPE}`
|
||||
: path;
|
||||
};
|
||||
|
||||
export const pathForTag = (tag: string) =>
|
||||
`${PREFIX_TAG}/${tag}`;
|
||||
|
||||
51
src/image-response/RecipeImageResponse.tsx
Normal file
51
src/image-response/RecipeImageResponse.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import type { Photo } from '../photo';
|
||||
import ImageCaption from './components/ImageCaption';
|
||||
import ImagePhotoGrid from './components/ImagePhotoGrid';
|
||||
import ImageContainer from './components/ImageContainer';
|
||||
import type { NextImageSize } from '@/platforms/next-image';
|
||||
import { formatTag } from '@/tag';
|
||||
import { TbChecklist } from 'react-icons/tb';
|
||||
|
||||
export default function RecipeImageResponse({
|
||||
recipe,
|
||||
photos,
|
||||
width,
|
||||
height,
|
||||
fontFamily,
|
||||
}: {
|
||||
recipe: string,
|
||||
photos: Photo[]
|
||||
width: NextImageSize
|
||||
height: number
|
||||
fontFamily: string
|
||||
}) {
|
||||
return (
|
||||
<ImageContainer {...{
|
||||
width,
|
||||
height,
|
||||
...photos.length === 0 && { background: 'black' },
|
||||
}}>
|
||||
<ImagePhotoGrid
|
||||
{...{
|
||||
photos,
|
||||
width,
|
||||
height,
|
||||
}}
|
||||
/>
|
||||
<ImageCaption {...{
|
||||
width,
|
||||
height,
|
||||
fontFamily,
|
||||
icon: <TbChecklist
|
||||
size={height * .087}
|
||||
style={{
|
||||
transform: `translateY(${height * .006}px)`,
|
||||
marginRight: height * .02,
|
||||
}}
|
||||
/>,
|
||||
}}>
|
||||
{formatTag(recipe).toLocaleUpperCase()}
|
||||
</ImageCaption>
|
||||
</ImageContainer>
|
||||
);
|
||||
}
|
||||
@ -11,6 +11,7 @@ import HiddenHeader from '@/tag/HiddenHeader';
|
||||
import FocalLengthHeader from '@/focal/FocalLengthHeader';
|
||||
import PhotoHeader from './PhotoHeader';
|
||||
import { JSX } from 'react';
|
||||
import RecipeHeader from '@/recipe/RecipeHeader';
|
||||
|
||||
export default function PhotoDetailPage({
|
||||
photo,
|
||||
@ -19,6 +20,7 @@ export default function PhotoDetailPage({
|
||||
tag,
|
||||
camera,
|
||||
simulation,
|
||||
recipe,
|
||||
focal,
|
||||
indexNumber,
|
||||
count,
|
||||
@ -72,6 +74,14 @@ export default function PhotoDetailPage({
|
||||
count={count}
|
||||
dateRange={dateRange}
|
||||
/>;
|
||||
} else if (recipe) {
|
||||
customHeader = <RecipeHeader
|
||||
recipe={recipe}
|
||||
photos={photos}
|
||||
selectedPhoto={photo}
|
||||
indexNumber={indexNumber}
|
||||
count={count}
|
||||
/>;
|
||||
} else if (focal) {
|
||||
customHeader = <FocalLengthHeader
|
||||
focal={focal}
|
||||
@ -90,6 +100,7 @@ export default function PhotoDetailPage({
|
||||
contentMain={customHeader ?? <PhotoHeader
|
||||
selectedPhoto={photo}
|
||||
photos={photos}
|
||||
recipe={recipe}
|
||||
/>}
|
||||
/>
|
||||
<AnimateItems
|
||||
@ -106,10 +117,12 @@ export default function PhotoDetailPage({
|
||||
showTitleAsH1
|
||||
showCamera={!camera}
|
||||
showSimulation={!simulation}
|
||||
showRecipe={!recipe}
|
||||
shouldShare={shouldShare}
|
||||
shouldShareTag={tag !== undefined}
|
||||
shouldShareCamera={camera !== undefined}
|
||||
shouldShareSimulation={simulation !== undefined}
|
||||
shouldShareRecipe={recipe !== undefined}
|
||||
includeFavoriteInAdminMenu={includeFavoriteInAdminMenu}
|
||||
/>,
|
||||
]}
|
||||
|
||||
@ -21,8 +21,6 @@ import {
|
||||
safelyParseFormattedHtml,
|
||||
} from '@/utility/html';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { convertTagToRecipe, isTagRecipe } from '@/recipe';
|
||||
import PhotoRecipe from '@/recipe/PhotoRecipe';
|
||||
|
||||
export default function PhotoGridSidebar({
|
||||
tags,
|
||||
@ -53,47 +51,35 @@ export default function PhotoGridSidebar({
|
||||
className="text-icon translate-y-[1px]"
|
||||
/>}
|
||||
items={tagsIncludingHidden.map(({ tag, count }) => {
|
||||
if (isTagRecipe(tag)) {
|
||||
return <PhotoRecipe
|
||||
key={tag}
|
||||
recipe={convertTagToRecipe(tag)}
|
||||
switch (tag) {
|
||||
case TAG_FAVS:
|
||||
return <FavsTag
|
||||
key={TAG_FAVS}
|
||||
countOnHover={count}
|
||||
type="icon-last"
|
||||
prefetch={false}
|
||||
contrast="low"
|
||||
badged
|
||||
/>;
|
||||
} else {
|
||||
switch (tag) {
|
||||
case TAG_FAVS:
|
||||
return <FavsTag
|
||||
key={TAG_FAVS}
|
||||
countOnHover={count}
|
||||
type="icon-last"
|
||||
prefetch={false}
|
||||
contrast="low"
|
||||
badged
|
||||
/>;
|
||||
case TAG_HIDDEN:
|
||||
return <HiddenTag
|
||||
key={TAG_HIDDEN}
|
||||
countOnHover={count}
|
||||
type="icon-last"
|
||||
prefetch={false}
|
||||
contrast="low"
|
||||
badged
|
||||
/>;
|
||||
default:
|
||||
return <PhotoTag
|
||||
key={tag}
|
||||
tag={tag}
|
||||
type="text-only"
|
||||
countOnHover={count}
|
||||
prefetch={false}
|
||||
contrast="low"
|
||||
badged
|
||||
/>;
|
||||
}
|
||||
case TAG_HIDDEN:
|
||||
return <HiddenTag
|
||||
key={TAG_HIDDEN}
|
||||
countOnHover={count}
|
||||
type="icon-last"
|
||||
prefetch={false}
|
||||
contrast="low"
|
||||
badged
|
||||
/>;
|
||||
default:
|
||||
return <PhotoTag
|
||||
key={tag}
|
||||
tag={tag}
|
||||
type="text-only"
|
||||
countOnHover={count}
|
||||
prefetch={false}
|
||||
contrast="low"
|
||||
badged
|
||||
/>;
|
||||
}
|
||||
})}
|
||||
/>
|
||||
|
||||
@ -22,6 +22,7 @@ export default function PhotoHeader({
|
||||
camera,
|
||||
simulation,
|
||||
focal,
|
||||
recipe,
|
||||
photos,
|
||||
selectedPhoto,
|
||||
entity,
|
||||
@ -68,6 +69,7 @@ export default function PhotoHeader({
|
||||
camera,
|
||||
simulation,
|
||||
focal,
|
||||
recipe,
|
||||
}} />;
|
||||
|
||||
const renderDateRange = () =>
|
||||
|
||||
@ -30,7 +30,7 @@ import {
|
||||
} from '@/app/config';
|
||||
import AdminPhotoMenuClient from '@/admin/AdminPhotoMenuClient';
|
||||
import { RevalidatePhoto } from './InfinitePhotoScroll';
|
||||
import { useRef } from 'react';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import useVisible from '@/utility/useVisible';
|
||||
import PhotoDate from './PhotoDate';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
@ -42,7 +42,8 @@ import { TbChecklist } from 'react-icons/tb';
|
||||
import { IoCloseSharp } from 'react-icons/io5';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import useRecipeState from '../recipe/useRecipeState';
|
||||
import PhotoRecipeGrid from '@/recipe/PhotoRecipeGrid';
|
||||
import PhotoRecipeOverlay from '@/recipe/PhotoRecipeGrid';
|
||||
import PhotoRecipe from '@/recipe/PhotoRecipe';
|
||||
|
||||
export default function PhotoLarge({
|
||||
photo,
|
||||
@ -56,12 +57,14 @@ export default function PhotoLarge({
|
||||
showTitleAsH1,
|
||||
showCamera = true,
|
||||
showSimulation = true,
|
||||
showRecipe = true,
|
||||
showZoomControls: showZoomControlsProp = true,
|
||||
shouldZoomOnFKeydown = true,
|
||||
shouldShare = true,
|
||||
shouldShareTag,
|
||||
shouldShareCamera,
|
||||
shouldShareSimulation,
|
||||
shouldShareRecipe,
|
||||
shouldShareFocalLength,
|
||||
includeFavoriteInAdminMenu,
|
||||
onVisible,
|
||||
@ -77,12 +80,14 @@ export default function PhotoLarge({
|
||||
showTitleAsH1?: boolean
|
||||
showCamera?: boolean
|
||||
showSimulation?: boolean
|
||||
showRecipe?: boolean
|
||||
showZoomControls?: boolean
|
||||
shouldZoomOnFKeydown?: boolean
|
||||
shouldShare?: boolean
|
||||
shouldShareTag?: boolean
|
||||
shouldShareCamera?: boolean
|
||||
shouldShareSimulation?: boolean
|
||||
shouldShareRecipe?: boolean
|
||||
shouldShareFocalLength?: boolean
|
||||
includeFavoriteInAdminMenu?: boolean
|
||||
onVisible?: () => void
|
||||
@ -101,21 +106,26 @@ export default function PhotoLarge({
|
||||
const showZoomControls = showZoomControlsProp && areZoomControlsShown;
|
||||
|
||||
const refRecipe = useRef<HTMLDivElement>(null);
|
||||
const refRecipeTrigger = useRef<HTMLButtonElement>(null);
|
||||
const refRecipeTitle = useRef<HTMLButtonElement>(null);
|
||||
const refRecipeButton = useRef<HTMLButtonElement>(null);
|
||||
const refTriggers = useMemo(() => [refRecipeTitle, refRecipeButton], []);
|
||||
const {
|
||||
shouldShowRecipe,
|
||||
toggleRecipe,
|
||||
hideRecipe,
|
||||
} = useRecipeState({
|
||||
ref: refRecipe,
|
||||
refTrigger: refRecipeTrigger,
|
||||
refTriggers,
|
||||
});
|
||||
|
||||
const tags = sortTags(photo.tags, primaryTag);
|
||||
|
||||
const camera = cameraFromPhoto(photo);
|
||||
|
||||
const { recipeTitle: recipe } = photo;
|
||||
|
||||
const showCameraContent = showCamera && shouldShowCameraDataForPhoto(photo);
|
||||
const showRecipeContent = showRecipe && recipe;
|
||||
const showTagsContent = tags.length > 0;
|
||||
const showExifContent = shouldShowExifDataForPhoto(photo);
|
||||
|
||||
@ -132,6 +142,7 @@ export default function PhotoLarge({
|
||||
const hasMetaContent =
|
||||
showCameraContent ||
|
||||
showTagsContent ||
|
||||
showRecipeContent ||
|
||||
showExifContent;
|
||||
|
||||
const hasNonDateContent =
|
||||
@ -185,11 +196,11 @@ export default function PhotoLarge({
|
||||
)}>
|
||||
<AnimatePresence>
|
||||
{(shouldShowRecipe || shouldDebugRecipeOverlays) &&
|
||||
photo.fujifilmRecipe &&
|
||||
photo.recipeData &&
|
||||
photo.filmSimulation &&
|
||||
<PhotoRecipeGrid
|
||||
<PhotoRecipeOverlay
|
||||
ref={refRecipe}
|
||||
recipe={photo.fujifilmRecipe}
|
||||
recipe={photo.recipeData}
|
||||
simulation={photo.filmSimulation}
|
||||
iso={photo.isoFormatted}
|
||||
exposure={photo.exposureCompensationFormatted}
|
||||
@ -250,7 +261,7 @@ export default function PhotoLarge({
|
||||
)}>
|
||||
{photo.caption}
|
||||
</div>}
|
||||
{(showCameraContent || showTagsContent) &&
|
||||
{(showCameraContent || showRecipeContent || showTagsContent) &&
|
||||
<div>
|
||||
{showCameraContent &&
|
||||
<PhotoCamera
|
||||
@ -258,6 +269,15 @@ export default function PhotoLarge({
|
||||
contrast="medium"
|
||||
prefetch={prefetchRelatedLinks}
|
||||
/>}
|
||||
{showRecipeContent &&
|
||||
<PhotoRecipe
|
||||
refButton={refRecipeTitle}
|
||||
recipe={recipe}
|
||||
contrast="medium"
|
||||
isOpen={shouldShowRecipe}
|
||||
recipeOnClick={toggleRecipe}
|
||||
prefetch={prefetchRelatedLinks}
|
||||
/>}
|
||||
{showTagsContent &&
|
||||
<PhotoTags
|
||||
tags={tags}
|
||||
@ -270,7 +290,7 @@ export default function PhotoLarge({
|
||||
{/* EXIF Data */}
|
||||
<div className={clsx(
|
||||
'space-y-baseline',
|
||||
!hasTitleContent && 'md:-mt-baseline',
|
||||
!hasTitleContent && !hasMetaContent && 'md:-mt-baseline',
|
||||
)}>
|
||||
{showExifContent &&
|
||||
<>
|
||||
@ -310,7 +330,7 @@ export default function PhotoLarge({
|
||||
</ul>
|
||||
{(
|
||||
(showSimulation && photo.filmSimulation) ||
|
||||
(SHOW_RECIPES && photo.fujifilmRecipe)
|
||||
(SHOW_RECIPES && showRecipe && photo.recipeData)
|
||||
) &&
|
||||
<div className="flex items-center gap-2 *:w-auto">
|
||||
{showSimulation && photo.filmSimulation &&
|
||||
@ -318,9 +338,9 @@ export default function PhotoLarge({
|
||||
simulation={photo.filmSimulation}
|
||||
prefetch={prefetchRelatedLinks}
|
||||
/>}
|
||||
{SHOW_RECIPES && photo.fujifilmRecipe &&
|
||||
{SHOW_RECIPES && photo.recipeData &&
|
||||
<button
|
||||
ref={refRecipeTrigger}
|
||||
ref={refRecipeButton}
|
||||
title="Fujifilm Recipe"
|
||||
onClick={toggleRecipe}
|
||||
className={clsx(
|
||||
@ -377,6 +397,9 @@ export default function PhotoLarge({
|
||||
simulation={shouldShareSimulation
|
||||
? photo.filmSimulation
|
||||
: undefined}
|
||||
recipe={shouldShareRecipe
|
||||
? recipe
|
||||
: undefined}
|
||||
focal={shouldShareFocalLength
|
||||
? photo.focalLength
|
||||
: undefined}
|
||||
|
||||
@ -28,6 +28,7 @@ export default function PhotoPrevNext({
|
||||
camera,
|
||||
simulation,
|
||||
focal,
|
||||
recipe,
|
||||
}: {
|
||||
photo?: Photo
|
||||
photos?: Photo[]
|
||||
@ -58,6 +59,7 @@ export default function PhotoPrevNext({
|
||||
camera,
|
||||
simulation,
|
||||
focal,
|
||||
recipe,
|
||||
}),
|
||||
{ scroll: false },
|
||||
);
|
||||
@ -71,9 +73,10 @@ export default function PhotoPrevNext({
|
||||
pathForPhoto({
|
||||
photo: nextPhoto,
|
||||
tag,
|
||||
camera,
|
||||
camera,
|
||||
simulation,
|
||||
focal,
|
||||
recipe,
|
||||
}),
|
||||
{ scroll: false },
|
||||
);
|
||||
@ -94,6 +97,7 @@ export default function PhotoPrevNext({
|
||||
camera,
|
||||
simulation,
|
||||
focal,
|
||||
recipe,
|
||||
]);
|
||||
|
||||
return (
|
||||
@ -114,6 +118,7 @@ export default function PhotoPrevNext({
|
||||
camera={camera}
|
||||
simulation={simulation}
|
||||
focal={focal}
|
||||
recipe={recipe}
|
||||
scroll={false}
|
||||
prefetch
|
||||
>
|
||||
@ -133,6 +138,7 @@ export default function PhotoPrevNext({
|
||||
camera={camera}
|
||||
simulation={simulation}
|
||||
focal={focal}
|
||||
recipe={recipe}
|
||||
scroll={false}
|
||||
prefetch
|
||||
>
|
||||
|
||||
@ -45,6 +45,7 @@ export const getWheresFromOptions = (
|
||||
camera,
|
||||
lens,
|
||||
simulation,
|
||||
recipe,
|
||||
focal,
|
||||
} = options;
|
||||
|
||||
@ -104,6 +105,10 @@ export const getWheresFromOptions = (
|
||||
wheres.push(`film_simulation=$${valuesIndex++}`);
|
||||
wheresValues.push(simulation);
|
||||
}
|
||||
if (recipe) {
|
||||
wheres.push(`recipe_title=$${valuesIndex++}`);
|
||||
wheresValues.push(recipe);
|
||||
}
|
||||
if (focal) {
|
||||
wheres.push(`focal_length=$${valuesIndex++}`);
|
||||
wheresValues.push(focal);
|
||||
|
||||
@ -23,11 +23,32 @@ export const MIGRATIONS: Migration[] = [{
|
||||
ADD COLUMN IF NOT EXISTS lens_model VARCHAR(255)
|
||||
`,
|
||||
}, {
|
||||
label: '03: Fujifilm Recipe',
|
||||
fields: ['fujifilm_recipe'],
|
||||
label: '03: Fujifilm Recipe: Data',
|
||||
fields: ['recipe_data'],
|
||||
run: () => sql`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS(
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name='photos'
|
||||
AND column_name='fujifilm_recipe'
|
||||
)
|
||||
THEN
|
||||
ALTER TABLE photos
|
||||
RENAME COLUMN fujifilm_recipe TO recipe_data;
|
||||
ELSE
|
||||
ALTER TABLE photos
|
||||
ADD COLUMN IF NOT EXISTS recipe_data JSONB;
|
||||
END IF;
|
||||
END $$;
|
||||
`,
|
||||
}, {
|
||||
label: '04: Fujifilm Recipe: Title',
|
||||
fields: ['recipe_title'],
|
||||
run: () => sql`
|
||||
ALTER TABLE photos
|
||||
ADD COLUMN IF NOT EXISTS fujifilm_recipe JSONB
|
||||
ADD COLUMN IF NOT EXISTS recipe_title VARCHAR(255)
|
||||
`,
|
||||
}];
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@ import { Lenses, createLensKey } from '@/lens';
|
||||
import { migrationForError } from './migration';
|
||||
import { UPDATED_BEFORE_01, UPDATED_BEFORE_02 } from '../outdated';
|
||||
import { MAKE_FUJIFILM } from '@/platforms/fujifilm';
|
||||
import { Recipes } from '@/recipe';
|
||||
|
||||
const createPhotosTable = () =>
|
||||
sql`
|
||||
@ -53,7 +54,8 @@ const createPhotosTable = () =>
|
||||
latitude DOUBLE PRECISION,
|
||||
longitude DOUBLE PRECISION,
|
||||
film_simulation VARCHAR(255),
|
||||
fujifilm_recipe JSONB,
|
||||
recipe_title VARCHAR(255),
|
||||
recipe_data JSONB,
|
||||
priority_order REAL,
|
||||
taken_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
taken_at_naive VARCHAR(255) NOT NULL,
|
||||
@ -76,6 +78,7 @@ const safelyQueryPhotos = async <T>(
|
||||
result = await callback();
|
||||
} catch (e: any) {
|
||||
const migration = migrationForError(e);
|
||||
console.log('Query error', e);
|
||||
if (migration) {
|
||||
console.log(`Running Migration ${migration.label} ...`);
|
||||
await migration.run();
|
||||
@ -140,7 +143,8 @@ export const insertPhoto = (photo: PhotoDbInsert) =>
|
||||
latitude,
|
||||
longitude,
|
||||
film_simulation,
|
||||
fujifilm_recipe,
|
||||
recipe_title,
|
||||
recipe_data,
|
||||
priority_order,
|
||||
hidden,
|
||||
taken_at,
|
||||
@ -170,7 +174,8 @@ export const insertPhoto = (photo: PhotoDbInsert) =>
|
||||
${photo.latitude},
|
||||
${photo.longitude},
|
||||
${photo.filmSimulation},
|
||||
${JSON.stringify(photo.fujifilmRecipe)},
|
||||
${photo.recipeTitle},
|
||||
${JSON.stringify(photo.recipeData)},
|
||||
${photo.priorityOrder},
|
||||
${photo.hidden},
|
||||
${photo.takenAt},
|
||||
@ -203,7 +208,8 @@ export const updatePhoto = (photo: PhotoDbInsert) =>
|
||||
latitude=${photo.latitude},
|
||||
longitude=${photo.longitude},
|
||||
film_simulation=${photo.filmSimulation},
|
||||
fujifilm_recipe=${JSON.stringify(photo.fujifilmRecipe)},
|
||||
recipe_title=${photo.recipeTitle},
|
||||
recipe_data=${JSON.stringify(photo.recipeData)},
|
||||
priority_order=${photo.priorityOrder || null},
|
||||
hidden=${photo.hidden},
|
||||
taken_at=${photo.takenAt},
|
||||
@ -325,6 +331,20 @@ export const getUniqueFilmSimulations = async () =>
|
||||
})))
|
||||
, 'getUniqueFilmSimulations');
|
||||
|
||||
export const getUniqueRecipes = async () =>
|
||||
safelyQueryPhotos(() => sql`
|
||||
SELECT DISTINCT recipe_title, COUNT(*)
|
||||
FROM photos
|
||||
WHERE hidden IS NOT TRUE AND recipe_title IS NOT NULL
|
||||
GROUP BY recipe_title
|
||||
ORDER BY recipe_title ASC
|
||||
`.then(({ rows }): Recipes => rows
|
||||
.map(({ recipe_title, count }) => ({
|
||||
recipe: recipe_title,
|
||||
count: parseInt(count, 10),
|
||||
})))
|
||||
, 'getUniqueRecipes');
|
||||
|
||||
export const getUniqueFocalLengths = async () =>
|
||||
safelyQueryPhotos(() => sql`
|
||||
SELECT DISTINCT focal_length, COUNT(*)
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
getOffsetFromExif,
|
||||
} from '@/utility/exif';
|
||||
import { roundToNumber } from '@/utility/number';
|
||||
import { convertStringToArray } from '@/utility/string';
|
||||
import { convertStringToArray, parameterize } from '@/utility/string';
|
||||
import { generateNanoid } from '@/utility/nanoid';
|
||||
import {
|
||||
FILM_SIMULATION_FORM_INPUT_OPTIONS,
|
||||
@ -111,9 +111,14 @@ const FORM_METADATA = (
|
||||
shouldHide: ({ make }) => make !== MAKE_FUJIFILM,
|
||||
shouldNotOverwriteWithNullDataOnSync: true,
|
||||
},
|
||||
fujifilmRecipe: {
|
||||
recipeTitle: {
|
||||
label: 'recipe title',
|
||||
spellCheck: false,
|
||||
capitalize: false,
|
||||
},
|
||||
recipeData: {
|
||||
type: 'textarea',
|
||||
label: 'fujifilm recipe',
|
||||
label: 'recipe data',
|
||||
spellCheck: false,
|
||||
capitalize: false,
|
||||
validate: value => {
|
||||
@ -207,7 +212,7 @@ export const convertPhotoToFormData = (photo: Photo): PhotoFormData => {
|
||||
return value?.toISOString ? value.toISOString() : value;
|
||||
case 'hidden':
|
||||
return value ? 'true' : 'false';
|
||||
case 'fujifilmRecipe':
|
||||
case 'recipeData':
|
||||
return JSON.stringify(value);
|
||||
default:
|
||||
return value !== undefined && value !== null
|
||||
@ -228,7 +233,7 @@ export const convertPhotoToFormData = (photo: Photo): PhotoFormData => {
|
||||
export const convertExifToFormData = (
|
||||
data: ExifData,
|
||||
filmSimulation?: FilmSimulation,
|
||||
fujifilmRecipe?: FujifilmRecipe,
|
||||
recipeData?: FujifilmRecipe,
|
||||
): Omit<
|
||||
Record<keyof PhotoExif, string | undefined>,
|
||||
'takenAt' | 'takenAtNaive'
|
||||
@ -252,7 +257,7 @@ export const convertExifToFormData = (
|
||||
longitude:
|
||||
!GEO_PRIVACY_ENABLED ? data.tags?.GPSLongitude?.toString() : undefined,
|
||||
filmSimulation,
|
||||
fujifilmRecipe: JSON.stringify(fujifilmRecipe),
|
||||
recipeData: JSON.stringify(recipeData),
|
||||
...data.tags?.DateTimeOriginal && {
|
||||
takenAt: convertTimestampWithOffsetToPostgresString(
|
||||
data.tags.DateTimeOriginal,
|
||||
@ -299,11 +304,14 @@ export const convertFormDataToPhotoDbInsert = (
|
||||
return {
|
||||
...(photoForm as PhotoFormData & {
|
||||
filmSimulation?: FilmSimulation
|
||||
fujifilmRecipe?: FujifilmRecipe
|
||||
recipeData?: FujifilmRecipe
|
||||
}),
|
||||
...!photoForm.id && { id: generateNanoid() },
|
||||
// Delete array field when empty
|
||||
tags: tags.length > 0 ? tags : undefined,
|
||||
...photoForm.recipeTitle && {
|
||||
recipeTitle: parameterize(photoForm.recipeTitle),
|
||||
},
|
||||
// Convert form strings to numbers
|
||||
aspectRatio: photoForm.aspectRatio
|
||||
? roundToNumber(parseFloat(photoForm.aspectRatio), 6)
|
||||
|
||||
@ -65,7 +65,7 @@ export interface PhotoExif {
|
||||
latitude?: number
|
||||
longitude?: number
|
||||
filmSimulation?: FilmSimulation
|
||||
fujifilmRecipe?: string
|
||||
recipeData?: string
|
||||
takenAt?: string
|
||||
takenAtNaive?: string
|
||||
}
|
||||
@ -80,6 +80,7 @@ export interface PhotoDbInsert extends PhotoExif {
|
||||
caption?: string
|
||||
semanticDescription?: string
|
||||
tags?: string[]
|
||||
recipeTitle?: string
|
||||
locationName?: string
|
||||
priorityOrder?: number
|
||||
hidden?: boolean
|
||||
@ -97,7 +98,7 @@ export interface PhotoDb extends
|
||||
}
|
||||
|
||||
// Parsed db response
|
||||
export interface Photo extends Omit<PhotoDb, 'fujifilmRecipe'> {
|
||||
export interface Photo extends Omit<PhotoDb, 'recipeData'> {
|
||||
focalLengthFormatted?: string
|
||||
focalLengthIn35MmFormatFormatted?: string
|
||||
fNumberFormatted?: string
|
||||
@ -105,7 +106,7 @@ export interface Photo extends Omit<PhotoDb, 'fujifilmRecipe'> {
|
||||
exposureTimeFormatted?: string
|
||||
exposureCompensationFormatted?: string
|
||||
takenAtNaiveFormatted: string
|
||||
fujifilmRecipe?: FujifilmRecipe
|
||||
recipeData?: FujifilmRecipe
|
||||
}
|
||||
|
||||
export interface PhotoSetCategory {
|
||||
@ -142,8 +143,8 @@ export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => {
|
||||
formatExposureTime(photoDb.exposureTime),
|
||||
exposureCompensationFormatted:
|
||||
formatExposureCompensation(photoDb.exposureCompensation),
|
||||
fujifilmRecipe: photoDb.fujifilmRecipe
|
||||
? JSON.parse(photoDb.fujifilmRecipe)
|
||||
recipeData: photoDb.recipeData
|
||||
? JSON.parse(photoDb.recipeData)
|
||||
: undefined,
|
||||
takenAtNaiveFormatted:
|
||||
formatDateFromPostgresString(photoDb.takenAtNaive),
|
||||
@ -165,7 +166,7 @@ export const convertPhotoToPhotoDbInsert = (
|
||||
): PhotoDbInsert => ({
|
||||
...photo,
|
||||
takenAt: photo.takenAt.toISOString(),
|
||||
fujifilmRecipe: JSON.stringify(photo.fujifilmRecipe),
|
||||
recipeData: JSON.stringify(photo.recipeData),
|
||||
});
|
||||
|
||||
export const photoStatsAsString = (photo: Photo) => [
|
||||
|
||||
@ -4,7 +4,8 @@ import EntityLink, {
|
||||
} from '@/components/primitives/EntityLink';
|
||||
import { TbChecklist } from 'react-icons/tb';
|
||||
import { formatRecipe } from '.';
|
||||
import clsx from 'clsx';
|
||||
import clsx from 'clsx/lite';
|
||||
import { RefObject } from 'react';
|
||||
|
||||
export default function PhotoRecipe({
|
||||
recipe,
|
||||
@ -14,9 +15,13 @@ export default function PhotoRecipe({
|
||||
prefetch,
|
||||
countOnHover,
|
||||
className,
|
||||
refButton,
|
||||
isOpen,
|
||||
recipeOnClick,
|
||||
}: {
|
||||
recipe: string
|
||||
refButton?: RefObject<HTMLButtonElement | null>
|
||||
isOpen?: boolean
|
||||
recipeOnClick?: () => void
|
||||
countOnHover?: number
|
||||
} & EntityLinkExternalProps) {
|
||||
@ -41,6 +46,7 @@ export default function PhotoRecipe({
|
||||
/>
|
||||
{recipeOnClick &&
|
||||
<button
|
||||
ref={refButton}
|
||||
onClick={recipeOnClick}
|
||||
className={clsx(
|
||||
'self-start',
|
||||
@ -48,7 +54,7 @@ export default function PhotoRecipe({
|
||||
'text-[10px] text-medium tracking-wider',
|
||||
)}
|
||||
>
|
||||
RECIPE
|
||||
{isOpen ? 'CLOSE' : 'RECIPE'}
|
||||
</button>}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -10,7 +10,7 @@ import { RecipeProps } from '.';
|
||||
|
||||
const addSign = (value = 0) => value < 0 ? value : `+${value}`;
|
||||
|
||||
export default function PhotoRecipeGrid({
|
||||
export default function PhotoRecipeOverlay({
|
||||
ref,
|
||||
recipe: {
|
||||
dynamicRange,
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
||||
import clsx from 'clsx/lite';
|
||||
import ImageLarge from '@/components/image/ImageLarge';
|
||||
import PhotoRecipeGrid from './PhotoRecipeGrid';
|
||||
import { Photo } from '../photo';
|
||||
import { useEffect, useState } from 'react';
|
||||
export default function PhotoRecipeOverlay({
|
||||
photos,
|
||||
recipe,
|
||||
className,
|
||||
}: {
|
||||
photos: Photo[]
|
||||
recipe: FujifilmRecipe
|
||||
className?: string
|
||||
}) {
|
||||
const [photoIndex, setPhotoIndex] = useState(0);
|
||||
|
||||
const photo = photos[photoIndex];
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setPhotoIndex((photoIndex + 1) % photos.length);
|
||||
}, 500);
|
||||
return () => clearInterval(interval);
|
||||
}, [photoIndex, photos]);
|
||||
|
||||
return (
|
||||
<div className={clsx(
|
||||
'relative w-full aspect-[3/2]',
|
||||
className,
|
||||
)}>
|
||||
<ImageLarge
|
||||
src={photo.url}
|
||||
alt="Image Background"
|
||||
aspectRatio={3 / 2}
|
||||
/>
|
||||
<div className={clsx(
|
||||
'absolute inset-0 w-full h-full',
|
||||
'flex items-center justify-center',
|
||||
)}>
|
||||
<PhotoRecipeGrid {...{
|
||||
recipe,
|
||||
simulation: photo.filmSimulation ?? 'provia',
|
||||
exposure: photo.exposureCompensationFormatted ?? '+0ev',
|
||||
iso: photo.isoFormatted ?? 'ISO 0',
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,10 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { Photo, PhotoDateRange } from '@/photo';
|
||||
import { descriptionForTaggedPhotos } from '../tag';
|
||||
import PhotoHeader from '@/photo/PhotoHeader';
|
||||
import PhotoRecipe from './PhotoRecipe';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import { descriptionForRecipePhotos } from '.';
|
||||
export default function RecipeHeader({
|
||||
recipe,
|
||||
photos,
|
||||
@ -22,27 +22,27 @@ export default function RecipeHeader({
|
||||
}) {
|
||||
const { setRecipeModalProps } = useAppState();
|
||||
|
||||
const photo = photos.find(({ filmSimulation, fujifilmRecipe }) =>
|
||||
fujifilmRecipe && filmSimulation);
|
||||
const photo = photos.find(({ filmSimulation, recipeData }) =>
|
||||
recipeData && filmSimulation);
|
||||
|
||||
return (
|
||||
<PhotoHeader
|
||||
tag={recipe}
|
||||
recipe={recipe}
|
||||
entity={<PhotoRecipe
|
||||
recipe={recipe}
|
||||
contrast="high"
|
||||
recipeOnClick={() => (
|
||||
photo?.fujifilmRecipe &&
|
||||
photo?.recipeData &&
|
||||
photo?.filmSimulation
|
||||
) ? setRecipeModalProps?.({
|
||||
simulation: photo.filmSimulation,
|
||||
recipe: photo.fujifilmRecipe,
|
||||
recipe: photo.recipeData,
|
||||
iso: photo.isoFormatted,
|
||||
exposure: photo.exposureTimeFormatted,
|
||||
})
|
||||
: undefined}
|
||||
/>}
|
||||
entityDescription={descriptionForTaggedPhotos(photos, undefined, count)}
|
||||
entityDescription={descriptionForRecipePhotos(photos, undefined, count)}
|
||||
photos={photos}
|
||||
selectedPhoto={selectedPhoto}
|
||||
indexNumber={indexNumber}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import Modal from '@/components/Modal';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import PhotoRecipeGrid from './PhotoRecipeGrid';
|
||||
import PhotoRecipeOverlay from './PhotoRecipeGrid';
|
||||
|
||||
export default function ShareModals() {
|
||||
const {
|
||||
@ -12,10 +12,11 @@ export default function ShareModals() {
|
||||
|
||||
if (recipeModalProps) {
|
||||
return <Modal
|
||||
className="bg-transparent!"
|
||||
onClose={() => setRecipeModalProps?.(undefined)}
|
||||
container={false}
|
||||
>
|
||||
<PhotoRecipeGrid {...{
|
||||
<PhotoRecipeOverlay {...{
|
||||
...recipeModalProps,
|
||||
onClose: () => setRecipeModalProps?.(undefined),
|
||||
}}/>
|
||||
|
||||
16
src/recipe/data.ts
Normal file
16
src/recipe/data.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import {
|
||||
getPhotosCached,
|
||||
getPhotosMetaCached,
|
||||
} from '@/photo/cache';
|
||||
|
||||
export const getPhotosRecipeDataCached = ({
|
||||
recipe,
|
||||
limit,
|
||||
}: {
|
||||
recipe: string,
|
||||
limit?: number,
|
||||
}) =>
|
||||
Promise.all([
|
||||
getPhotosCached({ recipe, limit }),
|
||||
getPhotosMetaCached({ recipe }),
|
||||
]);
|
||||
@ -1,13 +1,16 @@
|
||||
import { absolutePathForRecipe, absolutePathForRecipeImage } from '@/app/paths';
|
||||
import { descriptionForPhotoSet, Photo, photoQuantityText } from '@/photo';
|
||||
import { PhotoDateRange } from '@/photo';
|
||||
import { Tags } from '../tag';
|
||||
import { parameterize } from '@/utility/string';
|
||||
import { capitalizeWords } from '@/utility/string';
|
||||
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
||||
import { FilmSimulation } from '@/simulation';
|
||||
|
||||
const KEY_RECIPE = 'recipe';
|
||||
export type RecipeWithCount = {
|
||||
recipe: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export type Recipes = RecipeWithCount[]
|
||||
|
||||
export interface RecipeProps {
|
||||
recipe: FujifilmRecipe
|
||||
@ -16,19 +19,6 @@ export interface RecipeProps {
|
||||
exposure?: string
|
||||
}
|
||||
|
||||
export const isTagRecipe = (tag: string) =>
|
||||
(new RegExp(`^${KEY_RECIPE}-?`).test(tag));
|
||||
|
||||
export const convertTagsToRecipes = (tags: Tags) =>
|
||||
tags.filter(({ tag }) => isTagRecipe(tag))
|
||||
.map(({ tag }) => convertTagToRecipe(tag));
|
||||
|
||||
export const convertRecipeToTag = (recipe: string) =>
|
||||
`${KEY_RECIPE}-${parameterize(recipe)}`;
|
||||
|
||||
export const convertTagToRecipe = (tag: string) =>
|
||||
tag.replace(new RegExp(`^${KEY_RECIPE}-?`), '');
|
||||
|
||||
export const formatRecipe = (recipe?: string) =>
|
||||
capitalizeWords(recipe?.replaceAll('-', ' '));
|
||||
|
||||
|
||||
@ -1,20 +1,18 @@
|
||||
import {
|
||||
getPathComponents,
|
||||
pathForPhoto,
|
||||
SEARCH_PARAM_SHOW_RECIPE,
|
||||
} from '@/app/paths';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { SEARCH_PARAM_SHOW } from '@/app/paths';
|
||||
import { RefObject, useCallback, useEffect, useState } from 'react';
|
||||
import { isElementEntirelyInViewport } from '@/utility/dom';
|
||||
import useClickInsideOutside from '@/utility/useClickInsideOutside';
|
||||
|
||||
export default function useRecipeState({
|
||||
ref,
|
||||
refTrigger,
|
||||
refTriggers = [],
|
||||
}: {
|
||||
ref?: RefObject<HTMLElement | null>,
|
||||
refTrigger?: RefObject<HTMLElement | null>,
|
||||
refTriggers?: RefObject<HTMLElement | null>[],
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
|
||||
@ -23,13 +21,7 @@ export default function useRecipeState({
|
||||
...pathComponents
|
||||
} = getPathComponents(pathname);
|
||||
|
||||
const searchParamShow = typeof document !== 'undefined'
|
||||
? (new URLSearchParams(document.location.search)).get(SEARCH_PARAM_SHOW)
|
||||
: undefined;
|
||||
|
||||
const showRecipeInitially = searchParamShow === SEARCH_PARAM_SHOW_RECIPE;
|
||||
|
||||
const [shouldShowRecipe, setShouldShowRecipe] = useState(showRecipeInitially);
|
||||
const [shouldShowRecipe, setShouldShowRecipe] = useState(false);
|
||||
|
||||
const setVisibility = useCallback((shouldShow: boolean) => {
|
||||
if (shouldShow) {
|
||||
@ -69,7 +61,7 @@ export default function useRecipeState({
|
||||
[setVisibility, shouldShowRecipe]);
|
||||
|
||||
useClickInsideOutside({
|
||||
htmlElements: [ref, refTrigger],
|
||||
htmlElements: [ref, ...refTriggers],
|
||||
onClickOutside: hideRecipe,
|
||||
});
|
||||
|
||||
|
||||
@ -3,8 +3,6 @@ import { isTagFavs } from '.';
|
||||
import FavsTag from './FavsTag';
|
||||
import { EntityLinkExternalProps } from '@/components/primitives/EntityLink';
|
||||
import { Fragment } from 'react';
|
||||
import { convertTagToRecipe, isTagRecipe } from '@/recipe';
|
||||
import PhotoRecipe from '@/recipe/PhotoRecipe';
|
||||
|
||||
export default function PhotoTags({
|
||||
tags,
|
||||
@ -17,14 +15,9 @@ export default function PhotoTags({
|
||||
<div className="flex flex-col">
|
||||
{tags.map(tag =>
|
||||
<Fragment key={tag}>
|
||||
{isTagRecipe(tag)
|
||||
? <PhotoRecipe {...{
|
||||
recipe: convertTagToRecipe(tag),
|
||||
recipeOnClick: () => console.log('clicked'),
|
||||
}} />
|
||||
: isTagFavs(tag)
|
||||
? <FavsTag {...{ contrast, prefetch }} />
|
||||
: <PhotoTag {...{ tag, contrast, prefetch }} />}
|
||||
{isTagFavs(tag)
|
||||
? <FavsTag {...{ contrast, prefetch }} />
|
||||
: <PhotoTag {...{ tag, contrast, prefetch }} />}
|
||||
</Fragment>)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user