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 { STATICALLY_OPTIMIZED_PHOTO_CATEGORIES } from '@/app/config';
|
||||||
import { Metadata } from 'next/types';
|
import { Metadata } from 'next/types';
|
||||||
import { cache } from 'react';
|
import { cache } from 'react';
|
||||||
|
import { PATH_ROOT } from '@/app/paths';
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
const getPhotosFilmSimulationDataCachedCached =
|
const getPhotosFilmSimulationDataCachedCached =
|
||||||
cache(getPhotosFilmSimulationDataCached);
|
cache(getPhotosFilmSimulationDataCached);
|
||||||
@ -38,6 +40,8 @@ export async function generateMetadata({
|
|||||||
limit: INFINITE_SCROLL_GRID_INITIAL,
|
limit: INFINITE_SCROLL_GRID_INITIAL,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (photos.length === 0) { return {}; }
|
||||||
|
|
||||||
const {
|
const {
|
||||||
url,
|
url,
|
||||||
title,
|
title,
|
||||||
@ -75,6 +79,8 @@ export default async function FilmSimulationPage({
|
|||||||
limit: INFINITE_SCROLL_GRID_INITIAL,
|
limit: INFINITE_SCROLL_GRID_INITIAL,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (photos.length === 0) { redirect(PATH_ROOT); }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FilmSimulationOverview {...{
|
<FilmSimulationOverview {...{
|
||||||
simulation,
|
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 { 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 { IS_PRODUCTION } from '@/app/config';
|
||||||
import { STATICALLY_OPTIMIZED_PHOTO_CATEGORIES } from '@/app/config';
|
import { STATICALLY_OPTIMIZED_PHOTO_CATEGORIES } from '@/app/config';
|
||||||
import { PATH_ROOT } from '@/app/paths';
|
import { PATH_ROOT } from '@/app/paths';
|
||||||
import { getPhotosTagDataCached } from '@/tag/data';
|
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
import { cache } from 'react';
|
import { cache } from 'react';
|
||||||
import { convertRecipeToTag, generateMetaForRecipe } from '@/recipe';
|
import { generateMetaForRecipe } from '@/recipe';
|
||||||
import RecipeOverview from '@/recipe/RecipeOverview';
|
import RecipeOverview from '@/recipe/RecipeOverview';
|
||||||
|
import { getPhotosRecipeDataCached } from '@/recipe/data';
|
||||||
|
|
||||||
const getPhotosTagDataCachedCached = cache((tag: string) =>
|
const getPhotosRecipeDataCachedCached = cache(getPhotosRecipeDataCached);
|
||||||
getPhotosTagDataCached({ tag, limit: INFINITE_SCROLL_GRID_INITIAL}));
|
|
||||||
|
|
||||||
export let generateStaticParams:
|
export let generateStaticParams:
|
||||||
(() => Promise<{ recipe: string }[]>) | undefined = undefined;
|
(() => Promise<{ recipe: string }[]>) | undefined = undefined;
|
||||||
|
|
||||||
if (STATICALLY_OPTIMIZED_PHOTO_CATEGORIES && IS_PRODUCTION) {
|
if (STATICALLY_OPTIMIZED_PHOTO_CATEGORIES && IS_PRODUCTION) {
|
||||||
generateStaticParams = async () => {
|
generateStaticParams = async () => {
|
||||||
const tags = await getUniqueTags();
|
const recipes = await getUniqueRecipes();
|
||||||
return tags
|
return recipes.map(({ recipe }) => ({ recipe }));
|
||||||
.filter(({ tag }) => tag.startsWith('recipe'))
|
|
||||||
.map(({ tag }) => ({ recipe: tag.replace(/^recipe-?/i, '')}));
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,7 +36,10 @@ export async function generateMetadata({
|
|||||||
const [
|
const [
|
||||||
photos,
|
photos,
|
||||||
{ count, dateRange },
|
{ count, dateRange },
|
||||||
] = await getPhotosTagDataCachedCached(convertRecipeToTag(recipe));
|
] = await getPhotosRecipeDataCachedCached({
|
||||||
|
recipe,
|
||||||
|
limit: INFINITE_SCROLL_GRID_INITIAL,
|
||||||
|
});
|
||||||
|
|
||||||
if (photos.length === 0) { return {}; }
|
if (photos.length === 0) { return {}; }
|
||||||
|
|
||||||
@ -77,7 +77,10 @@ export default async function RecipePage({
|
|||||||
const [
|
const [
|
||||||
photos,
|
photos,
|
||||||
{ count, dateRange },
|
{ count, dateRange },
|
||||||
] = await getPhotosTagDataCachedCached(convertRecipeToTag(recipe));
|
] = await getPhotosRecipeDataCachedCached({
|
||||||
|
recipe,
|
||||||
|
limit: INFINITE_SCROLL_GRID_INITIAL,
|
||||||
|
});
|
||||||
|
|
||||||
if (photos.length === 0) { redirect(PATH_ROOT); }
|
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_FOCAL_LENGTH_DYNAMIC = `${PREFIX_FOCAL_LENGTH}/[focal]`;
|
||||||
const PATH_RECIPE_DYNAMIC = `${PREFIX_RECIPE}/[recipe]`;
|
const PATH_RECIPE_DYNAMIC = `${PREFIX_RECIPE}/[recipe]`;
|
||||||
|
|
||||||
// Search params
|
|
||||||
export const SEARCH_PARAM_SHOW = 'show';
|
|
||||||
export const SEARCH_PARAM_SHOW_RECIPE = 'recipe';
|
|
||||||
|
|
||||||
// Admin paths
|
// Admin paths
|
||||||
export const PATH_ADMIN_PHOTOS = `${PATH_ADMIN}/photos`;
|
export const PATH_ADMIN_PHOTOS = `${PATH_ADMIN}/photos`;
|
||||||
export const PATH_ADMIN_OUTDATED = `${PATH_ADMIN}/outdated`;
|
export const PATH_ADMIN_OUTDATED = `${PATH_ADMIN}/outdated`;
|
||||||
@ -114,9 +110,8 @@ export const pathForPhoto = ({
|
|||||||
simulation,
|
simulation,
|
||||||
focal,
|
focal,
|
||||||
recipe,
|
recipe,
|
||||||
showRecipe,
|
}: PhotoPathParams) =>
|
||||||
}: PhotoPathParams) => {
|
typeof photo !== 'string' && photo.hidden
|
||||||
const path = typeof photo !== 'string' && photo.hidden
|
|
||||||
? `${pathForTag(TAG_HIDDEN)}/${getPhotoId(photo)}`
|
? `${pathForTag(TAG_HIDDEN)}/${getPhotoId(photo)}`
|
||||||
: tag
|
: tag
|
||||||
? `${pathForTag(tag)}/${getPhotoId(photo)}`
|
? `${pathForTag(tag)}/${getPhotoId(photo)}`
|
||||||
@ -129,10 +124,6 @@ export const pathForPhoto = ({
|
|||||||
: recipe
|
: recipe
|
||||||
? `${pathForRecipe(recipe)}/${getPhotoId(photo)}`
|
? `${pathForRecipe(recipe)}/${getPhotoId(photo)}`
|
||||||
: `${PREFIX_PHOTO}/${getPhotoId(photo)}`;
|
: `${PREFIX_PHOTO}/${getPhotoId(photo)}`;
|
||||||
return showRecipe
|
|
||||||
? `${path}?${SEARCH_PARAM_SHOW}=${SEARCH_PARAM_SHOW_RECIPE}`
|
|
||||||
: path;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const pathForTag = (tag: string) =>
|
export const pathForTag = (tag: string) =>
|
||||||
`${PREFIX_TAG}/${tag}`;
|
`${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 FocalLengthHeader from '@/focal/FocalLengthHeader';
|
||||||
import PhotoHeader from './PhotoHeader';
|
import PhotoHeader from './PhotoHeader';
|
||||||
import { JSX } from 'react';
|
import { JSX } from 'react';
|
||||||
|
import RecipeHeader from '@/recipe/RecipeHeader';
|
||||||
|
|
||||||
export default function PhotoDetailPage({
|
export default function PhotoDetailPage({
|
||||||
photo,
|
photo,
|
||||||
@ -19,6 +20,7 @@ export default function PhotoDetailPage({
|
|||||||
tag,
|
tag,
|
||||||
camera,
|
camera,
|
||||||
simulation,
|
simulation,
|
||||||
|
recipe,
|
||||||
focal,
|
focal,
|
||||||
indexNumber,
|
indexNumber,
|
||||||
count,
|
count,
|
||||||
@ -72,6 +74,14 @@ export default function PhotoDetailPage({
|
|||||||
count={count}
|
count={count}
|
||||||
dateRange={dateRange}
|
dateRange={dateRange}
|
||||||
/>;
|
/>;
|
||||||
|
} else if (recipe) {
|
||||||
|
customHeader = <RecipeHeader
|
||||||
|
recipe={recipe}
|
||||||
|
photos={photos}
|
||||||
|
selectedPhoto={photo}
|
||||||
|
indexNumber={indexNumber}
|
||||||
|
count={count}
|
||||||
|
/>;
|
||||||
} else if (focal) {
|
} else if (focal) {
|
||||||
customHeader = <FocalLengthHeader
|
customHeader = <FocalLengthHeader
|
||||||
focal={focal}
|
focal={focal}
|
||||||
@ -90,6 +100,7 @@ export default function PhotoDetailPage({
|
|||||||
contentMain={customHeader ?? <PhotoHeader
|
contentMain={customHeader ?? <PhotoHeader
|
||||||
selectedPhoto={photo}
|
selectedPhoto={photo}
|
||||||
photos={photos}
|
photos={photos}
|
||||||
|
recipe={recipe}
|
||||||
/>}
|
/>}
|
||||||
/>
|
/>
|
||||||
<AnimateItems
|
<AnimateItems
|
||||||
@ -106,10 +117,12 @@ export default function PhotoDetailPage({
|
|||||||
showTitleAsH1
|
showTitleAsH1
|
||||||
showCamera={!camera}
|
showCamera={!camera}
|
||||||
showSimulation={!simulation}
|
showSimulation={!simulation}
|
||||||
|
showRecipe={!recipe}
|
||||||
shouldShare={shouldShare}
|
shouldShare={shouldShare}
|
||||||
shouldShareTag={tag !== undefined}
|
shouldShareTag={tag !== undefined}
|
||||||
shouldShareCamera={camera !== undefined}
|
shouldShareCamera={camera !== undefined}
|
||||||
shouldShareSimulation={simulation !== undefined}
|
shouldShareSimulation={simulation !== undefined}
|
||||||
|
shouldShareRecipe={recipe !== undefined}
|
||||||
includeFavoriteInAdminMenu={includeFavoriteInAdminMenu}
|
includeFavoriteInAdminMenu={includeFavoriteInAdminMenu}
|
||||||
/>,
|
/>,
|
||||||
]}
|
]}
|
||||||
|
|||||||
@ -21,8 +21,6 @@ import {
|
|||||||
safelyParseFormattedHtml,
|
safelyParseFormattedHtml,
|
||||||
} from '@/utility/html';
|
} from '@/utility/html';
|
||||||
import { clsx } from 'clsx/lite';
|
import { clsx } from 'clsx/lite';
|
||||||
import { convertTagToRecipe, isTagRecipe } from '@/recipe';
|
|
||||||
import PhotoRecipe from '@/recipe/PhotoRecipe';
|
|
||||||
|
|
||||||
export default function PhotoGridSidebar({
|
export default function PhotoGridSidebar({
|
||||||
tags,
|
tags,
|
||||||
@ -53,17 +51,6 @@ export default function PhotoGridSidebar({
|
|||||||
className="text-icon translate-y-[1px]"
|
className="text-icon translate-y-[1px]"
|
||||||
/>}
|
/>}
|
||||||
items={tagsIncludingHidden.map(({ tag, count }) => {
|
items={tagsIncludingHidden.map(({ tag, count }) => {
|
||||||
if (isTagRecipe(tag)) {
|
|
||||||
return <PhotoRecipe
|
|
||||||
key={tag}
|
|
||||||
recipe={convertTagToRecipe(tag)}
|
|
||||||
countOnHover={count}
|
|
||||||
type="icon-last"
|
|
||||||
prefetch={false}
|
|
||||||
contrast="low"
|
|
||||||
badged
|
|
||||||
/>;
|
|
||||||
} else {
|
|
||||||
switch (tag) {
|
switch (tag) {
|
||||||
case TAG_FAVS:
|
case TAG_FAVS:
|
||||||
return <FavsTag
|
return <FavsTag
|
||||||
@ -94,7 +81,6 @@ export default function PhotoGridSidebar({
|
|||||||
badged
|
badged
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
: null;
|
: null;
|
||||||
|
|||||||
@ -22,6 +22,7 @@ export default function PhotoHeader({
|
|||||||
camera,
|
camera,
|
||||||
simulation,
|
simulation,
|
||||||
focal,
|
focal,
|
||||||
|
recipe,
|
||||||
photos,
|
photos,
|
||||||
selectedPhoto,
|
selectedPhoto,
|
||||||
entity,
|
entity,
|
||||||
@ -68,6 +69,7 @@ export default function PhotoHeader({
|
|||||||
camera,
|
camera,
|
||||||
simulation,
|
simulation,
|
||||||
focal,
|
focal,
|
||||||
|
recipe,
|
||||||
}} />;
|
}} />;
|
||||||
|
|
||||||
const renderDateRange = () =>
|
const renderDateRange = () =>
|
||||||
|
|||||||
@ -30,7 +30,7 @@ import {
|
|||||||
} from '@/app/config';
|
} from '@/app/config';
|
||||||
import AdminPhotoMenuClient from '@/admin/AdminPhotoMenuClient';
|
import AdminPhotoMenuClient from '@/admin/AdminPhotoMenuClient';
|
||||||
import { RevalidatePhoto } from './InfinitePhotoScroll';
|
import { RevalidatePhoto } from './InfinitePhotoScroll';
|
||||||
import { useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
import useVisible from '@/utility/useVisible';
|
import useVisible from '@/utility/useVisible';
|
||||||
import PhotoDate from './PhotoDate';
|
import PhotoDate from './PhotoDate';
|
||||||
import { useAppState } from '@/state/AppState';
|
import { useAppState } from '@/state/AppState';
|
||||||
@ -42,7 +42,8 @@ import { TbChecklist } from 'react-icons/tb';
|
|||||||
import { IoCloseSharp } from 'react-icons/io5';
|
import { IoCloseSharp } from 'react-icons/io5';
|
||||||
import { AnimatePresence } from 'framer-motion';
|
import { AnimatePresence } from 'framer-motion';
|
||||||
import useRecipeState from '../recipe/useRecipeState';
|
import useRecipeState from '../recipe/useRecipeState';
|
||||||
import PhotoRecipeGrid from '@/recipe/PhotoRecipeGrid';
|
import PhotoRecipeOverlay from '@/recipe/PhotoRecipeGrid';
|
||||||
|
import PhotoRecipe from '@/recipe/PhotoRecipe';
|
||||||
|
|
||||||
export default function PhotoLarge({
|
export default function PhotoLarge({
|
||||||
photo,
|
photo,
|
||||||
@ -56,12 +57,14 @@ export default function PhotoLarge({
|
|||||||
showTitleAsH1,
|
showTitleAsH1,
|
||||||
showCamera = true,
|
showCamera = true,
|
||||||
showSimulation = true,
|
showSimulation = true,
|
||||||
|
showRecipe = true,
|
||||||
showZoomControls: showZoomControlsProp = true,
|
showZoomControls: showZoomControlsProp = true,
|
||||||
shouldZoomOnFKeydown = true,
|
shouldZoomOnFKeydown = true,
|
||||||
shouldShare = true,
|
shouldShare = true,
|
||||||
shouldShareTag,
|
shouldShareTag,
|
||||||
shouldShareCamera,
|
shouldShareCamera,
|
||||||
shouldShareSimulation,
|
shouldShareSimulation,
|
||||||
|
shouldShareRecipe,
|
||||||
shouldShareFocalLength,
|
shouldShareFocalLength,
|
||||||
includeFavoriteInAdminMenu,
|
includeFavoriteInAdminMenu,
|
||||||
onVisible,
|
onVisible,
|
||||||
@ -77,12 +80,14 @@ export default function PhotoLarge({
|
|||||||
showTitleAsH1?: boolean
|
showTitleAsH1?: boolean
|
||||||
showCamera?: boolean
|
showCamera?: boolean
|
||||||
showSimulation?: boolean
|
showSimulation?: boolean
|
||||||
|
showRecipe?: boolean
|
||||||
showZoomControls?: boolean
|
showZoomControls?: boolean
|
||||||
shouldZoomOnFKeydown?: boolean
|
shouldZoomOnFKeydown?: boolean
|
||||||
shouldShare?: boolean
|
shouldShare?: boolean
|
||||||
shouldShareTag?: boolean
|
shouldShareTag?: boolean
|
||||||
shouldShareCamera?: boolean
|
shouldShareCamera?: boolean
|
||||||
shouldShareSimulation?: boolean
|
shouldShareSimulation?: boolean
|
||||||
|
shouldShareRecipe?: boolean
|
||||||
shouldShareFocalLength?: boolean
|
shouldShareFocalLength?: boolean
|
||||||
includeFavoriteInAdminMenu?: boolean
|
includeFavoriteInAdminMenu?: boolean
|
||||||
onVisible?: () => void
|
onVisible?: () => void
|
||||||
@ -101,21 +106,26 @@ export default function PhotoLarge({
|
|||||||
const showZoomControls = showZoomControlsProp && areZoomControlsShown;
|
const showZoomControls = showZoomControlsProp && areZoomControlsShown;
|
||||||
|
|
||||||
const refRecipe = useRef<HTMLDivElement>(null);
|
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 {
|
const {
|
||||||
shouldShowRecipe,
|
shouldShowRecipe,
|
||||||
toggleRecipe,
|
toggleRecipe,
|
||||||
hideRecipe,
|
hideRecipe,
|
||||||
} = useRecipeState({
|
} = useRecipeState({
|
||||||
ref: refRecipe,
|
ref: refRecipe,
|
||||||
refTrigger: refRecipeTrigger,
|
refTriggers,
|
||||||
});
|
});
|
||||||
|
|
||||||
const tags = sortTags(photo.tags, primaryTag);
|
const tags = sortTags(photo.tags, primaryTag);
|
||||||
|
|
||||||
const camera = cameraFromPhoto(photo);
|
const camera = cameraFromPhoto(photo);
|
||||||
|
|
||||||
|
const { recipeTitle: recipe } = photo;
|
||||||
|
|
||||||
const showCameraContent = showCamera && shouldShowCameraDataForPhoto(photo);
|
const showCameraContent = showCamera && shouldShowCameraDataForPhoto(photo);
|
||||||
|
const showRecipeContent = showRecipe && recipe;
|
||||||
const showTagsContent = tags.length > 0;
|
const showTagsContent = tags.length > 0;
|
||||||
const showExifContent = shouldShowExifDataForPhoto(photo);
|
const showExifContent = shouldShowExifDataForPhoto(photo);
|
||||||
|
|
||||||
@ -132,6 +142,7 @@ export default function PhotoLarge({
|
|||||||
const hasMetaContent =
|
const hasMetaContent =
|
||||||
showCameraContent ||
|
showCameraContent ||
|
||||||
showTagsContent ||
|
showTagsContent ||
|
||||||
|
showRecipeContent ||
|
||||||
showExifContent;
|
showExifContent;
|
||||||
|
|
||||||
const hasNonDateContent =
|
const hasNonDateContent =
|
||||||
@ -185,11 +196,11 @@ export default function PhotoLarge({
|
|||||||
)}>
|
)}>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{(shouldShowRecipe || shouldDebugRecipeOverlays) &&
|
{(shouldShowRecipe || shouldDebugRecipeOverlays) &&
|
||||||
photo.fujifilmRecipe &&
|
photo.recipeData &&
|
||||||
photo.filmSimulation &&
|
photo.filmSimulation &&
|
||||||
<PhotoRecipeGrid
|
<PhotoRecipeOverlay
|
||||||
ref={refRecipe}
|
ref={refRecipe}
|
||||||
recipe={photo.fujifilmRecipe}
|
recipe={photo.recipeData}
|
||||||
simulation={photo.filmSimulation}
|
simulation={photo.filmSimulation}
|
||||||
iso={photo.isoFormatted}
|
iso={photo.isoFormatted}
|
||||||
exposure={photo.exposureCompensationFormatted}
|
exposure={photo.exposureCompensationFormatted}
|
||||||
@ -250,7 +261,7 @@ export default function PhotoLarge({
|
|||||||
)}>
|
)}>
|
||||||
{photo.caption}
|
{photo.caption}
|
||||||
</div>}
|
</div>}
|
||||||
{(showCameraContent || showTagsContent) &&
|
{(showCameraContent || showRecipeContent || showTagsContent) &&
|
||||||
<div>
|
<div>
|
||||||
{showCameraContent &&
|
{showCameraContent &&
|
||||||
<PhotoCamera
|
<PhotoCamera
|
||||||
@ -258,6 +269,15 @@ export default function PhotoLarge({
|
|||||||
contrast="medium"
|
contrast="medium"
|
||||||
prefetch={prefetchRelatedLinks}
|
prefetch={prefetchRelatedLinks}
|
||||||
/>}
|
/>}
|
||||||
|
{showRecipeContent &&
|
||||||
|
<PhotoRecipe
|
||||||
|
refButton={refRecipeTitle}
|
||||||
|
recipe={recipe}
|
||||||
|
contrast="medium"
|
||||||
|
isOpen={shouldShowRecipe}
|
||||||
|
recipeOnClick={toggleRecipe}
|
||||||
|
prefetch={prefetchRelatedLinks}
|
||||||
|
/>}
|
||||||
{showTagsContent &&
|
{showTagsContent &&
|
||||||
<PhotoTags
|
<PhotoTags
|
||||||
tags={tags}
|
tags={tags}
|
||||||
@ -270,7 +290,7 @@ export default function PhotoLarge({
|
|||||||
{/* EXIF Data */}
|
{/* EXIF Data */}
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
'space-y-baseline',
|
'space-y-baseline',
|
||||||
!hasTitleContent && 'md:-mt-baseline',
|
!hasTitleContent && !hasMetaContent && 'md:-mt-baseline',
|
||||||
)}>
|
)}>
|
||||||
{showExifContent &&
|
{showExifContent &&
|
||||||
<>
|
<>
|
||||||
@ -310,7 +330,7 @@ export default function PhotoLarge({
|
|||||||
</ul>
|
</ul>
|
||||||
{(
|
{(
|
||||||
(showSimulation && photo.filmSimulation) ||
|
(showSimulation && photo.filmSimulation) ||
|
||||||
(SHOW_RECIPES && photo.fujifilmRecipe)
|
(SHOW_RECIPES && showRecipe && photo.recipeData)
|
||||||
) &&
|
) &&
|
||||||
<div className="flex items-center gap-2 *:w-auto">
|
<div className="flex items-center gap-2 *:w-auto">
|
||||||
{showSimulation && photo.filmSimulation &&
|
{showSimulation && photo.filmSimulation &&
|
||||||
@ -318,9 +338,9 @@ export default function PhotoLarge({
|
|||||||
simulation={photo.filmSimulation}
|
simulation={photo.filmSimulation}
|
||||||
prefetch={prefetchRelatedLinks}
|
prefetch={prefetchRelatedLinks}
|
||||||
/>}
|
/>}
|
||||||
{SHOW_RECIPES && photo.fujifilmRecipe &&
|
{SHOW_RECIPES && photo.recipeData &&
|
||||||
<button
|
<button
|
||||||
ref={refRecipeTrigger}
|
ref={refRecipeButton}
|
||||||
title="Fujifilm Recipe"
|
title="Fujifilm Recipe"
|
||||||
onClick={toggleRecipe}
|
onClick={toggleRecipe}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@ -377,6 +397,9 @@ export default function PhotoLarge({
|
|||||||
simulation={shouldShareSimulation
|
simulation={shouldShareSimulation
|
||||||
? photo.filmSimulation
|
? photo.filmSimulation
|
||||||
: undefined}
|
: undefined}
|
||||||
|
recipe={shouldShareRecipe
|
||||||
|
? recipe
|
||||||
|
: undefined}
|
||||||
focal={shouldShareFocalLength
|
focal={shouldShareFocalLength
|
||||||
? photo.focalLength
|
? photo.focalLength
|
||||||
: undefined}
|
: undefined}
|
||||||
|
|||||||
@ -28,6 +28,7 @@ export default function PhotoPrevNext({
|
|||||||
camera,
|
camera,
|
||||||
simulation,
|
simulation,
|
||||||
focal,
|
focal,
|
||||||
|
recipe,
|
||||||
}: {
|
}: {
|
||||||
photo?: Photo
|
photo?: Photo
|
||||||
photos?: Photo[]
|
photos?: Photo[]
|
||||||
@ -58,6 +59,7 @@ export default function PhotoPrevNext({
|
|||||||
camera,
|
camera,
|
||||||
simulation,
|
simulation,
|
||||||
focal,
|
focal,
|
||||||
|
recipe,
|
||||||
}),
|
}),
|
||||||
{ scroll: false },
|
{ scroll: false },
|
||||||
);
|
);
|
||||||
@ -74,6 +76,7 @@ export default function PhotoPrevNext({
|
|||||||
camera,
|
camera,
|
||||||
simulation,
|
simulation,
|
||||||
focal,
|
focal,
|
||||||
|
recipe,
|
||||||
}),
|
}),
|
||||||
{ scroll: false },
|
{ scroll: false },
|
||||||
);
|
);
|
||||||
@ -94,6 +97,7 @@ export default function PhotoPrevNext({
|
|||||||
camera,
|
camera,
|
||||||
simulation,
|
simulation,
|
||||||
focal,
|
focal,
|
||||||
|
recipe,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -114,6 +118,7 @@ export default function PhotoPrevNext({
|
|||||||
camera={camera}
|
camera={camera}
|
||||||
simulation={simulation}
|
simulation={simulation}
|
||||||
focal={focal}
|
focal={focal}
|
||||||
|
recipe={recipe}
|
||||||
scroll={false}
|
scroll={false}
|
||||||
prefetch
|
prefetch
|
||||||
>
|
>
|
||||||
@ -133,6 +138,7 @@ export default function PhotoPrevNext({
|
|||||||
camera={camera}
|
camera={camera}
|
||||||
simulation={simulation}
|
simulation={simulation}
|
||||||
focal={focal}
|
focal={focal}
|
||||||
|
recipe={recipe}
|
||||||
scroll={false}
|
scroll={false}
|
||||||
prefetch
|
prefetch
|
||||||
>
|
>
|
||||||
|
|||||||
@ -45,6 +45,7 @@ export const getWheresFromOptions = (
|
|||||||
camera,
|
camera,
|
||||||
lens,
|
lens,
|
||||||
simulation,
|
simulation,
|
||||||
|
recipe,
|
||||||
focal,
|
focal,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
@ -104,6 +105,10 @@ export const getWheresFromOptions = (
|
|||||||
wheres.push(`film_simulation=$${valuesIndex++}`);
|
wheres.push(`film_simulation=$${valuesIndex++}`);
|
||||||
wheresValues.push(simulation);
|
wheresValues.push(simulation);
|
||||||
}
|
}
|
||||||
|
if (recipe) {
|
||||||
|
wheres.push(`recipe_title=$${valuesIndex++}`);
|
||||||
|
wheresValues.push(recipe);
|
||||||
|
}
|
||||||
if (focal) {
|
if (focal) {
|
||||||
wheres.push(`focal_length=$${valuesIndex++}`);
|
wheres.push(`focal_length=$${valuesIndex++}`);
|
||||||
wheresValues.push(focal);
|
wheresValues.push(focal);
|
||||||
|
|||||||
@ -23,11 +23,32 @@ export const MIGRATIONS: Migration[] = [{
|
|||||||
ADD COLUMN IF NOT EXISTS lens_model VARCHAR(255)
|
ADD COLUMN IF NOT EXISTS lens_model VARCHAR(255)
|
||||||
`,
|
`,
|
||||||
}, {
|
}, {
|
||||||
label: '03: Fujifilm Recipe',
|
label: '03: Fujifilm Recipe: Data',
|
||||||
fields: ['fujifilm_recipe'],
|
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`
|
run: () => sql`
|
||||||
ALTER TABLE photos
|
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 { migrationForError } from './migration';
|
||||||
import { UPDATED_BEFORE_01, UPDATED_BEFORE_02 } from '../outdated';
|
import { UPDATED_BEFORE_01, UPDATED_BEFORE_02 } from '../outdated';
|
||||||
import { MAKE_FUJIFILM } from '@/platforms/fujifilm';
|
import { MAKE_FUJIFILM } from '@/platforms/fujifilm';
|
||||||
|
import { Recipes } from '@/recipe';
|
||||||
|
|
||||||
const createPhotosTable = () =>
|
const createPhotosTable = () =>
|
||||||
sql`
|
sql`
|
||||||
@ -53,7 +54,8 @@ const createPhotosTable = () =>
|
|||||||
latitude DOUBLE PRECISION,
|
latitude DOUBLE PRECISION,
|
||||||
longitude DOUBLE PRECISION,
|
longitude DOUBLE PRECISION,
|
||||||
film_simulation VARCHAR(255),
|
film_simulation VARCHAR(255),
|
||||||
fujifilm_recipe JSONB,
|
recipe_title VARCHAR(255),
|
||||||
|
recipe_data JSONB,
|
||||||
priority_order REAL,
|
priority_order REAL,
|
||||||
taken_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
taken_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
taken_at_naive VARCHAR(255) NOT NULL,
|
taken_at_naive VARCHAR(255) NOT NULL,
|
||||||
@ -76,6 +78,7 @@ const safelyQueryPhotos = async <T>(
|
|||||||
result = await callback();
|
result = await callback();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
const migration = migrationForError(e);
|
const migration = migrationForError(e);
|
||||||
|
console.log('Query error', e);
|
||||||
if (migration) {
|
if (migration) {
|
||||||
console.log(`Running Migration ${migration.label} ...`);
|
console.log(`Running Migration ${migration.label} ...`);
|
||||||
await migration.run();
|
await migration.run();
|
||||||
@ -140,7 +143,8 @@ export const insertPhoto = (photo: PhotoDbInsert) =>
|
|||||||
latitude,
|
latitude,
|
||||||
longitude,
|
longitude,
|
||||||
film_simulation,
|
film_simulation,
|
||||||
fujifilm_recipe,
|
recipe_title,
|
||||||
|
recipe_data,
|
||||||
priority_order,
|
priority_order,
|
||||||
hidden,
|
hidden,
|
||||||
taken_at,
|
taken_at,
|
||||||
@ -170,7 +174,8 @@ export const insertPhoto = (photo: PhotoDbInsert) =>
|
|||||||
${photo.latitude},
|
${photo.latitude},
|
||||||
${photo.longitude},
|
${photo.longitude},
|
||||||
${photo.filmSimulation},
|
${photo.filmSimulation},
|
||||||
${JSON.stringify(photo.fujifilmRecipe)},
|
${photo.recipeTitle},
|
||||||
|
${JSON.stringify(photo.recipeData)},
|
||||||
${photo.priorityOrder},
|
${photo.priorityOrder},
|
||||||
${photo.hidden},
|
${photo.hidden},
|
||||||
${photo.takenAt},
|
${photo.takenAt},
|
||||||
@ -203,7 +208,8 @@ export const updatePhoto = (photo: PhotoDbInsert) =>
|
|||||||
latitude=${photo.latitude},
|
latitude=${photo.latitude},
|
||||||
longitude=${photo.longitude},
|
longitude=${photo.longitude},
|
||||||
film_simulation=${photo.filmSimulation},
|
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},
|
priority_order=${photo.priorityOrder || null},
|
||||||
hidden=${photo.hidden},
|
hidden=${photo.hidden},
|
||||||
taken_at=${photo.takenAt},
|
taken_at=${photo.takenAt},
|
||||||
@ -325,6 +331,20 @@ export const getUniqueFilmSimulations = async () =>
|
|||||||
})))
|
})))
|
||||||
, 'getUniqueFilmSimulations');
|
, '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 () =>
|
export const getUniqueFocalLengths = async () =>
|
||||||
safelyQueryPhotos(() => sql`
|
safelyQueryPhotos(() => sql`
|
||||||
SELECT DISTINCT focal_length, COUNT(*)
|
SELECT DISTINCT focal_length, COUNT(*)
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
getOffsetFromExif,
|
getOffsetFromExif,
|
||||||
} from '@/utility/exif';
|
} from '@/utility/exif';
|
||||||
import { roundToNumber } from '@/utility/number';
|
import { roundToNumber } from '@/utility/number';
|
||||||
import { convertStringToArray } from '@/utility/string';
|
import { convertStringToArray, parameterize } from '@/utility/string';
|
||||||
import { generateNanoid } from '@/utility/nanoid';
|
import { generateNanoid } from '@/utility/nanoid';
|
||||||
import {
|
import {
|
||||||
FILM_SIMULATION_FORM_INPUT_OPTIONS,
|
FILM_SIMULATION_FORM_INPUT_OPTIONS,
|
||||||
@ -111,9 +111,14 @@ const FORM_METADATA = (
|
|||||||
shouldHide: ({ make }) => make !== MAKE_FUJIFILM,
|
shouldHide: ({ make }) => make !== MAKE_FUJIFILM,
|
||||||
shouldNotOverwriteWithNullDataOnSync: true,
|
shouldNotOverwriteWithNullDataOnSync: true,
|
||||||
},
|
},
|
||||||
fujifilmRecipe: {
|
recipeTitle: {
|
||||||
|
label: 'recipe title',
|
||||||
|
spellCheck: false,
|
||||||
|
capitalize: false,
|
||||||
|
},
|
||||||
|
recipeData: {
|
||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
label: 'fujifilm recipe',
|
label: 'recipe data',
|
||||||
spellCheck: false,
|
spellCheck: false,
|
||||||
capitalize: false,
|
capitalize: false,
|
||||||
validate: value => {
|
validate: value => {
|
||||||
@ -207,7 +212,7 @@ export const convertPhotoToFormData = (photo: Photo): PhotoFormData => {
|
|||||||
return value?.toISOString ? value.toISOString() : value;
|
return value?.toISOString ? value.toISOString() : value;
|
||||||
case 'hidden':
|
case 'hidden':
|
||||||
return value ? 'true' : 'false';
|
return value ? 'true' : 'false';
|
||||||
case 'fujifilmRecipe':
|
case 'recipeData':
|
||||||
return JSON.stringify(value);
|
return JSON.stringify(value);
|
||||||
default:
|
default:
|
||||||
return value !== undefined && value !== null
|
return value !== undefined && value !== null
|
||||||
@ -228,7 +233,7 @@ export const convertPhotoToFormData = (photo: Photo): PhotoFormData => {
|
|||||||
export const convertExifToFormData = (
|
export const convertExifToFormData = (
|
||||||
data: ExifData,
|
data: ExifData,
|
||||||
filmSimulation?: FilmSimulation,
|
filmSimulation?: FilmSimulation,
|
||||||
fujifilmRecipe?: FujifilmRecipe,
|
recipeData?: FujifilmRecipe,
|
||||||
): Omit<
|
): Omit<
|
||||||
Record<keyof PhotoExif, string | undefined>,
|
Record<keyof PhotoExif, string | undefined>,
|
||||||
'takenAt' | 'takenAtNaive'
|
'takenAt' | 'takenAtNaive'
|
||||||
@ -252,7 +257,7 @@ export const convertExifToFormData = (
|
|||||||
longitude:
|
longitude:
|
||||||
!GEO_PRIVACY_ENABLED ? data.tags?.GPSLongitude?.toString() : undefined,
|
!GEO_PRIVACY_ENABLED ? data.tags?.GPSLongitude?.toString() : undefined,
|
||||||
filmSimulation,
|
filmSimulation,
|
||||||
fujifilmRecipe: JSON.stringify(fujifilmRecipe),
|
recipeData: JSON.stringify(recipeData),
|
||||||
...data.tags?.DateTimeOriginal && {
|
...data.tags?.DateTimeOriginal && {
|
||||||
takenAt: convertTimestampWithOffsetToPostgresString(
|
takenAt: convertTimestampWithOffsetToPostgresString(
|
||||||
data.tags.DateTimeOriginal,
|
data.tags.DateTimeOriginal,
|
||||||
@ -299,11 +304,14 @@ export const convertFormDataToPhotoDbInsert = (
|
|||||||
return {
|
return {
|
||||||
...(photoForm as PhotoFormData & {
|
...(photoForm as PhotoFormData & {
|
||||||
filmSimulation?: FilmSimulation
|
filmSimulation?: FilmSimulation
|
||||||
fujifilmRecipe?: FujifilmRecipe
|
recipeData?: FujifilmRecipe
|
||||||
}),
|
}),
|
||||||
...!photoForm.id && { id: generateNanoid() },
|
...!photoForm.id && { id: generateNanoid() },
|
||||||
// Delete array field when empty
|
// Delete array field when empty
|
||||||
tags: tags.length > 0 ? tags : undefined,
|
tags: tags.length > 0 ? tags : undefined,
|
||||||
|
...photoForm.recipeTitle && {
|
||||||
|
recipeTitle: parameterize(photoForm.recipeTitle),
|
||||||
|
},
|
||||||
// Convert form strings to numbers
|
// Convert form strings to numbers
|
||||||
aspectRatio: photoForm.aspectRatio
|
aspectRatio: photoForm.aspectRatio
|
||||||
? roundToNumber(parseFloat(photoForm.aspectRatio), 6)
|
? roundToNumber(parseFloat(photoForm.aspectRatio), 6)
|
||||||
|
|||||||
@ -65,7 +65,7 @@ export interface PhotoExif {
|
|||||||
latitude?: number
|
latitude?: number
|
||||||
longitude?: number
|
longitude?: number
|
||||||
filmSimulation?: FilmSimulation
|
filmSimulation?: FilmSimulation
|
||||||
fujifilmRecipe?: string
|
recipeData?: string
|
||||||
takenAt?: string
|
takenAt?: string
|
||||||
takenAtNaive?: string
|
takenAtNaive?: string
|
||||||
}
|
}
|
||||||
@ -80,6 +80,7 @@ export interface PhotoDbInsert extends PhotoExif {
|
|||||||
caption?: string
|
caption?: string
|
||||||
semanticDescription?: string
|
semanticDescription?: string
|
||||||
tags?: string[]
|
tags?: string[]
|
||||||
|
recipeTitle?: string
|
||||||
locationName?: string
|
locationName?: string
|
||||||
priorityOrder?: number
|
priorityOrder?: number
|
||||||
hidden?: boolean
|
hidden?: boolean
|
||||||
@ -97,7 +98,7 @@ export interface PhotoDb extends
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parsed db response
|
// Parsed db response
|
||||||
export interface Photo extends Omit<PhotoDb, 'fujifilmRecipe'> {
|
export interface Photo extends Omit<PhotoDb, 'recipeData'> {
|
||||||
focalLengthFormatted?: string
|
focalLengthFormatted?: string
|
||||||
focalLengthIn35MmFormatFormatted?: string
|
focalLengthIn35MmFormatFormatted?: string
|
||||||
fNumberFormatted?: string
|
fNumberFormatted?: string
|
||||||
@ -105,7 +106,7 @@ export interface Photo extends Omit<PhotoDb, 'fujifilmRecipe'> {
|
|||||||
exposureTimeFormatted?: string
|
exposureTimeFormatted?: string
|
||||||
exposureCompensationFormatted?: string
|
exposureCompensationFormatted?: string
|
||||||
takenAtNaiveFormatted: string
|
takenAtNaiveFormatted: string
|
||||||
fujifilmRecipe?: FujifilmRecipe
|
recipeData?: FujifilmRecipe
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PhotoSetCategory {
|
export interface PhotoSetCategory {
|
||||||
@ -142,8 +143,8 @@ export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => {
|
|||||||
formatExposureTime(photoDb.exposureTime),
|
formatExposureTime(photoDb.exposureTime),
|
||||||
exposureCompensationFormatted:
|
exposureCompensationFormatted:
|
||||||
formatExposureCompensation(photoDb.exposureCompensation),
|
formatExposureCompensation(photoDb.exposureCompensation),
|
||||||
fujifilmRecipe: photoDb.fujifilmRecipe
|
recipeData: photoDb.recipeData
|
||||||
? JSON.parse(photoDb.fujifilmRecipe)
|
? JSON.parse(photoDb.recipeData)
|
||||||
: undefined,
|
: undefined,
|
||||||
takenAtNaiveFormatted:
|
takenAtNaiveFormatted:
|
||||||
formatDateFromPostgresString(photoDb.takenAtNaive),
|
formatDateFromPostgresString(photoDb.takenAtNaive),
|
||||||
@ -165,7 +166,7 @@ export const convertPhotoToPhotoDbInsert = (
|
|||||||
): PhotoDbInsert => ({
|
): PhotoDbInsert => ({
|
||||||
...photo,
|
...photo,
|
||||||
takenAt: photo.takenAt.toISOString(),
|
takenAt: photo.takenAt.toISOString(),
|
||||||
fujifilmRecipe: JSON.stringify(photo.fujifilmRecipe),
|
recipeData: JSON.stringify(photo.recipeData),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const photoStatsAsString = (photo: Photo) => [
|
export const photoStatsAsString = (photo: Photo) => [
|
||||||
|
|||||||
@ -4,7 +4,8 @@ import EntityLink, {
|
|||||||
} from '@/components/primitives/EntityLink';
|
} from '@/components/primitives/EntityLink';
|
||||||
import { TbChecklist } from 'react-icons/tb';
|
import { TbChecklist } from 'react-icons/tb';
|
||||||
import { formatRecipe } from '.';
|
import { formatRecipe } from '.';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx/lite';
|
||||||
|
import { RefObject } from 'react';
|
||||||
|
|
||||||
export default function PhotoRecipe({
|
export default function PhotoRecipe({
|
||||||
recipe,
|
recipe,
|
||||||
@ -14,9 +15,13 @@ export default function PhotoRecipe({
|
|||||||
prefetch,
|
prefetch,
|
||||||
countOnHover,
|
countOnHover,
|
||||||
className,
|
className,
|
||||||
|
refButton,
|
||||||
|
isOpen,
|
||||||
recipeOnClick,
|
recipeOnClick,
|
||||||
}: {
|
}: {
|
||||||
recipe: string
|
recipe: string
|
||||||
|
refButton?: RefObject<HTMLButtonElement | null>
|
||||||
|
isOpen?: boolean
|
||||||
recipeOnClick?: () => void
|
recipeOnClick?: () => void
|
||||||
countOnHover?: number
|
countOnHover?: number
|
||||||
} & EntityLinkExternalProps) {
|
} & EntityLinkExternalProps) {
|
||||||
@ -41,6 +46,7 @@ export default function PhotoRecipe({
|
|||||||
/>
|
/>
|
||||||
{recipeOnClick &&
|
{recipeOnClick &&
|
||||||
<button
|
<button
|
||||||
|
ref={refButton}
|
||||||
onClick={recipeOnClick}
|
onClick={recipeOnClick}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'self-start',
|
'self-start',
|
||||||
@ -48,7 +54,7 @@ export default function PhotoRecipe({
|
|||||||
'text-[10px] text-medium tracking-wider',
|
'text-[10px] text-medium tracking-wider',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
RECIPE
|
{isOpen ? 'CLOSE' : 'RECIPE'}
|
||||||
</button>}
|
</button>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { RecipeProps } from '.';
|
|||||||
|
|
||||||
const addSign = (value = 0) => value < 0 ? value : `+${value}`;
|
const addSign = (value = 0) => value < 0 ? value : `+${value}`;
|
||||||
|
|
||||||
export default function PhotoRecipeGrid({
|
export default function PhotoRecipeOverlay({
|
||||||
ref,
|
ref,
|
||||||
recipe: {
|
recipe: {
|
||||||
dynamicRange,
|
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';
|
'use client';
|
||||||
|
|
||||||
import { Photo, PhotoDateRange } from '@/photo';
|
import { Photo, PhotoDateRange } from '@/photo';
|
||||||
import { descriptionForTaggedPhotos } from '../tag';
|
|
||||||
import PhotoHeader from '@/photo/PhotoHeader';
|
import PhotoHeader from '@/photo/PhotoHeader';
|
||||||
import PhotoRecipe from './PhotoRecipe';
|
import PhotoRecipe from './PhotoRecipe';
|
||||||
import { useAppState } from '@/state/AppState';
|
import { useAppState } from '@/state/AppState';
|
||||||
|
import { descriptionForRecipePhotos } from '.';
|
||||||
export default function RecipeHeader({
|
export default function RecipeHeader({
|
||||||
recipe,
|
recipe,
|
||||||
photos,
|
photos,
|
||||||
@ -22,27 +22,27 @@ export default function RecipeHeader({
|
|||||||
}) {
|
}) {
|
||||||
const { setRecipeModalProps } = useAppState();
|
const { setRecipeModalProps } = useAppState();
|
||||||
|
|
||||||
const photo = photos.find(({ filmSimulation, fujifilmRecipe }) =>
|
const photo = photos.find(({ filmSimulation, recipeData }) =>
|
||||||
fujifilmRecipe && filmSimulation);
|
recipeData && filmSimulation);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PhotoHeader
|
<PhotoHeader
|
||||||
tag={recipe}
|
recipe={recipe}
|
||||||
entity={<PhotoRecipe
|
entity={<PhotoRecipe
|
||||||
recipe={recipe}
|
recipe={recipe}
|
||||||
contrast="high"
|
contrast="high"
|
||||||
recipeOnClick={() => (
|
recipeOnClick={() => (
|
||||||
photo?.fujifilmRecipe &&
|
photo?.recipeData &&
|
||||||
photo?.filmSimulation
|
photo?.filmSimulation
|
||||||
) ? setRecipeModalProps?.({
|
) ? setRecipeModalProps?.({
|
||||||
simulation: photo.filmSimulation,
|
simulation: photo.filmSimulation,
|
||||||
recipe: photo.fujifilmRecipe,
|
recipe: photo.recipeData,
|
||||||
iso: photo.isoFormatted,
|
iso: photo.isoFormatted,
|
||||||
exposure: photo.exposureTimeFormatted,
|
exposure: photo.exposureTimeFormatted,
|
||||||
})
|
})
|
||||||
: undefined}
|
: undefined}
|
||||||
/>}
|
/>}
|
||||||
entityDescription={descriptionForTaggedPhotos(photos, undefined, count)}
|
entityDescription={descriptionForRecipePhotos(photos, undefined, count)}
|
||||||
photos={photos}
|
photos={photos}
|
||||||
selectedPhoto={selectedPhoto}
|
selectedPhoto={selectedPhoto}
|
||||||
indexNumber={indexNumber}
|
indexNumber={indexNumber}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import Modal from '@/components/Modal';
|
import Modal from '@/components/Modal';
|
||||||
import { useAppState } from '@/state/AppState';
|
import { useAppState } from '@/state/AppState';
|
||||||
import PhotoRecipeGrid from './PhotoRecipeGrid';
|
import PhotoRecipeOverlay from './PhotoRecipeGrid';
|
||||||
|
|
||||||
export default function ShareModals() {
|
export default function ShareModals() {
|
||||||
const {
|
const {
|
||||||
@ -12,10 +12,11 @@ export default function ShareModals() {
|
|||||||
|
|
||||||
if (recipeModalProps) {
|
if (recipeModalProps) {
|
||||||
return <Modal
|
return <Modal
|
||||||
|
className="bg-transparent!"
|
||||||
onClose={() => setRecipeModalProps?.(undefined)}
|
onClose={() => setRecipeModalProps?.(undefined)}
|
||||||
container={false}
|
container={false}
|
||||||
>
|
>
|
||||||
<PhotoRecipeGrid {...{
|
<PhotoRecipeOverlay {...{
|
||||||
...recipeModalProps,
|
...recipeModalProps,
|
||||||
onClose: () => setRecipeModalProps?.(undefined),
|
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 { absolutePathForRecipe, absolutePathForRecipeImage } from '@/app/paths';
|
||||||
import { descriptionForPhotoSet, Photo, photoQuantityText } from '@/photo';
|
import { descriptionForPhotoSet, Photo, photoQuantityText } from '@/photo';
|
||||||
import { PhotoDateRange } from '@/photo';
|
import { PhotoDateRange } from '@/photo';
|
||||||
import { Tags } from '../tag';
|
|
||||||
import { parameterize } from '@/utility/string';
|
|
||||||
import { capitalizeWords } from '@/utility/string';
|
import { capitalizeWords } from '@/utility/string';
|
||||||
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
||||||
import { FilmSimulation } from '@/simulation';
|
import { FilmSimulation } from '@/simulation';
|
||||||
|
|
||||||
const KEY_RECIPE = 'recipe';
|
export type RecipeWithCount = {
|
||||||
|
recipe: string
|
||||||
|
count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Recipes = RecipeWithCount[]
|
||||||
|
|
||||||
export interface RecipeProps {
|
export interface RecipeProps {
|
||||||
recipe: FujifilmRecipe
|
recipe: FujifilmRecipe
|
||||||
@ -16,19 +19,6 @@ export interface RecipeProps {
|
|||||||
exposure?: string
|
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) =>
|
export const formatRecipe = (recipe?: string) =>
|
||||||
capitalizeWords(recipe?.replaceAll('-', ' '));
|
capitalizeWords(recipe?.replaceAll('-', ' '));
|
||||||
|
|
||||||
|
|||||||
@ -1,20 +1,18 @@
|
|||||||
import {
|
import {
|
||||||
getPathComponents,
|
getPathComponents,
|
||||||
pathForPhoto,
|
pathForPhoto,
|
||||||
SEARCH_PARAM_SHOW_RECIPE,
|
|
||||||
} from '@/app/paths';
|
} from '@/app/paths';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { SEARCH_PARAM_SHOW } from '@/app/paths';
|
|
||||||
import { RefObject, useCallback, useEffect, useState } from 'react';
|
import { RefObject, useCallback, useEffect, useState } from 'react';
|
||||||
import { isElementEntirelyInViewport } from '@/utility/dom';
|
import { isElementEntirelyInViewport } from '@/utility/dom';
|
||||||
import useClickInsideOutside from '@/utility/useClickInsideOutside';
|
import useClickInsideOutside from '@/utility/useClickInsideOutside';
|
||||||
|
|
||||||
export default function useRecipeState({
|
export default function useRecipeState({
|
||||||
ref,
|
ref,
|
||||||
refTrigger,
|
refTriggers = [],
|
||||||
}: {
|
}: {
|
||||||
ref?: RefObject<HTMLElement | null>,
|
ref?: RefObject<HTMLElement | null>,
|
||||||
refTrigger?: RefObject<HTMLElement | null>,
|
refTriggers?: RefObject<HTMLElement | null>[],
|
||||||
}) {
|
}) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
@ -23,13 +21,7 @@ export default function useRecipeState({
|
|||||||
...pathComponents
|
...pathComponents
|
||||||
} = getPathComponents(pathname);
|
} = getPathComponents(pathname);
|
||||||
|
|
||||||
const searchParamShow = typeof document !== 'undefined'
|
const [shouldShowRecipe, setShouldShowRecipe] = useState(false);
|
||||||
? (new URLSearchParams(document.location.search)).get(SEARCH_PARAM_SHOW)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const showRecipeInitially = searchParamShow === SEARCH_PARAM_SHOW_RECIPE;
|
|
||||||
|
|
||||||
const [shouldShowRecipe, setShouldShowRecipe] = useState(showRecipeInitially);
|
|
||||||
|
|
||||||
const setVisibility = useCallback((shouldShow: boolean) => {
|
const setVisibility = useCallback((shouldShow: boolean) => {
|
||||||
if (shouldShow) {
|
if (shouldShow) {
|
||||||
@ -69,7 +61,7 @@ export default function useRecipeState({
|
|||||||
[setVisibility, shouldShowRecipe]);
|
[setVisibility, shouldShowRecipe]);
|
||||||
|
|
||||||
useClickInsideOutside({
|
useClickInsideOutside({
|
||||||
htmlElements: [ref, refTrigger],
|
htmlElements: [ref, ...refTriggers],
|
||||||
onClickOutside: hideRecipe,
|
onClickOutside: hideRecipe,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -3,8 +3,6 @@ import { isTagFavs } from '.';
|
|||||||
import FavsTag from './FavsTag';
|
import FavsTag from './FavsTag';
|
||||||
import { EntityLinkExternalProps } from '@/components/primitives/EntityLink';
|
import { EntityLinkExternalProps } from '@/components/primitives/EntityLink';
|
||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
import { convertTagToRecipe, isTagRecipe } from '@/recipe';
|
|
||||||
import PhotoRecipe from '@/recipe/PhotoRecipe';
|
|
||||||
|
|
||||||
export default function PhotoTags({
|
export default function PhotoTags({
|
||||||
tags,
|
tags,
|
||||||
@ -17,12 +15,7 @@ export default function PhotoTags({
|
|||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
{tags.map(tag =>
|
{tags.map(tag =>
|
||||||
<Fragment key={tag}>
|
<Fragment key={tag}>
|
||||||
{isTagRecipe(tag)
|
{isTagFavs(tag)
|
||||||
? <PhotoRecipe {...{
|
|
||||||
recipe: convertTagToRecipe(tag),
|
|
||||||
recipeOnClick: () => console.log('clicked'),
|
|
||||||
}} />
|
|
||||||
: isTagFavs(tag)
|
|
||||||
? <FavsTag {...{ contrast, prefetch }} />
|
? <FavsTag {...{ contrast, prefetch }} />
|
||||||
: <PhotoTag {...{ tag, contrast, prefetch }} />}
|
: <PhotoTag {...{ tag, contrast, prefetch }} />}
|
||||||
</Fragment>)}
|
</Fragment>)}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user