commit
1eae6093f9
@ -1,28 +0,0 @@
|
||||
import SiteGrid from '@/components/SiteGrid';
|
||||
import { getPhoto, getPhotos } from '@/photo/db/query';
|
||||
import PhotoRecipeOverlay from '@/photo/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 '@/photo/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,
|
||||
|
||||
@ -28,6 +28,7 @@ export default async function GridPage() {
|
||||
tags,
|
||||
cameras,
|
||||
simulations,
|
||||
recipes,
|
||||
] = await Promise.all([
|
||||
getPhotosCached()
|
||||
.catch(() => []),
|
||||
@ -40,7 +41,7 @@ export default async function GridPage() {
|
||||
return (
|
||||
photos.length > 0
|
||||
? <PhotoGridPage
|
||||
{...{ photos, photosCount, tags, cameras, simulations }}
|
||||
{...{ photos, photosCount, tags, cameras, simulations, recipes }}
|
||||
/>
|
||||
: <PhotosEmptyState />
|
||||
);
|
||||
|
||||
@ -22,6 +22,7 @@ import AdminBatchEditPanel from '@/admin/AdminBatchEditPanel';
|
||||
import ShareModals from '@/share/ShareModals';
|
||||
import AdminUploadPanel from '@/admin/upload/AdminUploadPanel';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import RecipeModal from '@/recipe/RecipeModal';
|
||||
|
||||
import '../tailwind.css';
|
||||
|
||||
@ -86,6 +87,7 @@ export default function RootLayout({
|
||||
)}>
|
||||
<Nav siteDomainOrTitle={SITE_DOMAIN_OR_TITLE} />
|
||||
<ShareModals />
|
||||
<RecipeModal />
|
||||
<div className={clsx(
|
||||
'min-h-[16rem] sm:min-h-[30rem]',
|
||||
'mb-12',
|
||||
|
||||
@ -34,6 +34,7 @@ export default async function HomePage() {
|
||||
tags,
|
||||
cameras,
|
||||
simulations,
|
||||
recipes,
|
||||
] = await Promise.all([
|
||||
getPhotosCached()
|
||||
.catch(() => []),
|
||||
@ -42,14 +43,14 @@ export default async function HomePage() {
|
||||
.catch(() => 0),
|
||||
...(GRID_HOMEPAGE_ENABLED
|
||||
? getPhotoSidebarData()
|
||||
: [[], [], []]),
|
||||
: [[], [], [], []]),
|
||||
]);
|
||||
|
||||
return (
|
||||
photos.length > 0
|
||||
? GRID_HOMEPAGE_ENABLED
|
||||
? <PhotoGridPage
|
||||
{...{ photos, photosCount, tags, cameras, simulations }}
|
||||
{...{ photos, photosCount, tags, cameras, simulations, recipes }}
|
||||
/>
|
||||
: <PhotoFeedPage
|
||||
{...{ photos, photosCount }}
|
||||
|
||||
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 },
|
||||
);
|
||||
}
|
||||
90
app/recipe/[recipe]/page.tsx
Normal file
90
app/recipe/[recipe]/page.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo';
|
||||
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 type { Metadata } from 'next';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { cache } from 'react';
|
||||
import { generateMetaForRecipe } from '@/recipe';
|
||||
import RecipeOverview from '@/recipe/RecipeOverview';
|
||||
import { getPhotosRecipeDataCached } from '@/recipe/data';
|
||||
|
||||
const getPhotosRecipeDataCachedCached = cache(getPhotosRecipeDataCached);
|
||||
|
||||
export let generateStaticParams:
|
||||
(() => Promise<{ recipe: string }[]>) | undefined = undefined;
|
||||
|
||||
if (STATICALLY_OPTIMIZED_PHOTO_CATEGORIES && IS_PRODUCTION) {
|
||||
generateStaticParams = async () => {
|
||||
const recipes = await getUniqueRecipes();
|
||||
return recipes.map(({ recipe }) => ({ recipe }));
|
||||
};
|
||||
}
|
||||
|
||||
interface RecipeProps {
|
||||
params: Promise<{ recipe: string }>
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: RecipeProps): Promise<Metadata> {
|
||||
const { recipe: recipeFromParams } = await params;
|
||||
|
||||
const recipe = decodeURIComponent(recipeFromParams);
|
||||
|
||||
const [
|
||||
photos,
|
||||
{ count, dateRange },
|
||||
] = await getPhotosRecipeDataCachedCached({
|
||||
recipe,
|
||||
limit: INFINITE_SCROLL_GRID_INITIAL,
|
||||
});
|
||||
|
||||
if (photos.length === 0) { return {}; }
|
||||
|
||||
const {
|
||||
url,
|
||||
title,
|
||||
description,
|
||||
images,
|
||||
} = generateMetaForRecipe(recipe, photos, count, dateRange);
|
||||
|
||||
return {
|
||||
title,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
images,
|
||||
url,
|
||||
},
|
||||
twitter: {
|
||||
images,
|
||||
description,
|
||||
card: 'summary_large_image',
|
||||
},
|
||||
description,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function RecipePage({
|
||||
params,
|
||||
}:RecipeProps) {
|
||||
const { recipe: recipeFromParams } = await params;
|
||||
|
||||
const recipe = decodeURIComponent(recipeFromParams);
|
||||
|
||||
const [
|
||||
photos,
|
||||
{ count, dateRange },
|
||||
] = await getPhotosRecipeDataCachedCached({
|
||||
recipe,
|
||||
limit: INFINITE_SCROLL_GRID_INITIAL,
|
||||
});
|
||||
|
||||
if (photos.length === 0) { redirect(PATH_ROOT); }
|
||||
|
||||
return (
|
||||
<RecipeOverview {...{ recipe, photos, count, dateRange }} />
|
||||
);
|
||||
}
|
||||
14
package.json
14
package.json
@ -21,15 +21,15 @@
|
||||
"@vercel/analytics": "^1.5.0",
|
||||
"@vercel/blob": "^0.27.2",
|
||||
"@vercel/speed-insights": "^1.2.0",
|
||||
"ai": "^4.1.47",
|
||||
"ai": "^4.1.50",
|
||||
"camelcase-keys": "^9.1.3",
|
||||
"cmdk": "^1.0.4",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"exifr": "^7.1.3",
|
||||
"framer-motion": "^12.4.7",
|
||||
"framer-motion": "^12.4.10",
|
||||
"nanoid": "^5.1.2",
|
||||
"next": "15.1.7",
|
||||
"next": "15.2.1",
|
||||
"next-auth": "5.0.0-beta.25",
|
||||
"next-themes": "^0.4.4",
|
||||
"pg": "^8.13.3",
|
||||
@ -46,8 +46,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.0",
|
||||
"@next/bundle-analyzer": "15.1.7",
|
||||
"@next/eslint-plugin-next": "^15.1.7",
|
||||
"@next/bundle-analyzer": "15.2.1",
|
||||
"@next/eslint-plugin-next": "^15.2.1",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tailwindcss/postcss": "^4.0.9",
|
||||
@ -55,7 +55,7 @@
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.13.8",
|
||||
"@types/node": "^22.13.9",
|
||||
"@types/pg": "^8.11.11",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
@ -63,7 +63,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"cross-fetch": "^4.1.0",
|
||||
"eslint": "9.21.0",
|
||||
"eslint-config-next": "15.1.7",
|
||||
"eslint-config-next": "15.2.1",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
|
||||
269
pnpm-lock.yaml
generated
269
pnpm-lock.yaml
generated
@ -37,16 +37,16 @@ importers:
|
||||
version: 1.34.4
|
||||
'@vercel/analytics':
|
||||
specifier: ^1.5.0
|
||||
version: 1.5.0(next@15.1.7(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
|
||||
version: 1.5.0(next@15.2.1(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
|
||||
'@vercel/blob':
|
||||
specifier: ^0.27.2
|
||||
version: 0.27.2
|
||||
'@vercel/speed-insights':
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.0(next@15.1.7(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
|
||||
version: 1.2.0(next@15.2.1(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
|
||||
ai:
|
||||
specifier: ^4.1.47
|
||||
version: 4.1.47(react@19.0.0)(zod@3.24.2)
|
||||
specifier: ^4.1.50
|
||||
version: 4.1.50(react@19.0.0)(zod@3.24.2)
|
||||
camelcase-keys:
|
||||
specifier: ^9.1.3
|
||||
version: 9.1.3
|
||||
@ -63,17 +63,17 @@ importers:
|
||||
specifier: ^7.1.3
|
||||
version: 7.1.3
|
||||
framer-motion:
|
||||
specifier: ^12.4.7
|
||||
version: 12.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
specifier: ^12.4.10
|
||||
version: 12.4.10(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
nanoid:
|
||||
specifier: ^5.1.2
|
||||
version: 5.1.2
|
||||
next:
|
||||
specifier: 15.1.7
|
||||
version: 15.1.7(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
specifier: 15.2.1
|
||||
version: 15.2.1(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
next-auth:
|
||||
specifier: 5.0.0-beta.25
|
||||
version: 5.0.0-beta.25(next@15.1.7(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
|
||||
version: 5.0.0-beta.25(next@15.2.1(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
|
||||
next-themes:
|
||||
specifier: ^0.4.4
|
||||
version: 0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
@ -115,11 +115,11 @@ importers:
|
||||
specifier: ^3.3.0
|
||||
version: 3.3.0
|
||||
'@next/bundle-analyzer':
|
||||
specifier: 15.1.7
|
||||
version: 15.1.7
|
||||
specifier: 15.2.1
|
||||
version: 15.2.1
|
||||
'@next/eslint-plugin-next':
|
||||
specifier: ^15.1.7
|
||||
version: 15.2.0
|
||||
specifier: ^15.2.1
|
||||
version: 15.2.1
|
||||
'@tailwindcss/container-queries':
|
||||
specifier: ^0.1.1
|
||||
version: 0.1.1(tailwindcss@4.0.9)
|
||||
@ -142,8 +142,8 @@ importers:
|
||||
specifier: ^29.5.14
|
||||
version: 29.5.14
|
||||
'@types/node':
|
||||
specifier: ^22.13.8
|
||||
version: 22.13.8
|
||||
specifier: ^22.13.9
|
||||
version: 22.13.9
|
||||
'@types/pg':
|
||||
specifier: ^8.11.11
|
||||
version: 8.11.11
|
||||
@ -166,14 +166,14 @@ importers:
|
||||
specifier: 9.21.0
|
||||
version: 9.21.0(jiti@2.4.2)
|
||||
eslint-config-next:
|
||||
specifier: 15.1.7
|
||||
version: 15.1.7(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2)
|
||||
specifier: 15.2.1
|
||||
version: 15.2.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2)
|
||||
eslint-plugin-react-hooks:
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.0(eslint@9.21.0(jiti@2.4.2))
|
||||
jest:
|
||||
specifier: ^29.7.0
|
||||
version: 29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2))
|
||||
version: 29.7.0(@types/node@22.13.9)(ts-node@10.9.2(@types/node@22.13.9)(typescript@5.8.2))
|
||||
jest-environment-jsdom:
|
||||
specifier: ^29.7.0
|
||||
version: 29.7.0
|
||||
@ -185,7 +185,7 @@ importers:
|
||||
version: 4.0.9
|
||||
ts-node:
|
||||
specifier: ^10.9.2
|
||||
version: 10.9.2(@types/node@22.13.8)(typescript@5.8.2)
|
||||
version: 10.9.2(@types/node@22.13.9)(typescript@5.8.2)
|
||||
typescript:
|
||||
specifier: 5.8.2
|
||||
version: 5.8.2
|
||||
@ -214,8 +214,8 @@ packages:
|
||||
resolution: {integrity: sha512-jie6ZJT2ZR0uVOVCDc9R2xCX5I/Dum/wEK28lx21PJx6ZnFAN9EzD2WsPhcDWfCgGx3OAZZ0GyM3CEobXpa9LA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@ai-sdk/react@1.1.19':
|
||||
resolution: {integrity: sha512-zqSOWmJxpB45ZrwZ04+Q7Uo3xeGM0tva2eUYz2T4gv9Yk6MQAfOBA6+scsKg8CyIuUy4M4/C4pCY3eWQf7sfQg==}
|
||||
'@ai-sdk/react@1.1.20':
|
||||
resolution: {integrity: sha512-4QOM9fR9SryaRraybckDjrhl1O6XejqELdKmrM5g9y9eLnWAfjwF+W1aN0knkSHzbbjMqN77sy9B9yL8EuJbDw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
@ -865,62 +865,59 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.9':
|
||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||
|
||||
'@next/bundle-analyzer@15.1.7':
|
||||
resolution: {integrity: sha512-tESiAwTUEpzzxKMLDbQuPHvD+PFDjY+0O3R4T5bpjIo0cr5fvppbbllQbtksQbBEquT55Eu8JmDoOlc9YFv6Kw==}
|
||||
'@next/bundle-analyzer@15.2.1':
|
||||
resolution: {integrity: sha512-RSegG5zFEy+Pp771fvXu+LmNMmf6kuCSuliXAiLFogLz+QUNw9mUZ0U9fxA/2mngHRUmANsSCGv7l0RkyXjnxg==}
|
||||
|
||||
'@next/env@15.1.7':
|
||||
resolution: {integrity: sha512-d9jnRrkuOH7Mhi+LHav2XW91HOgTAWHxjMPkXMGBc9B2b7614P7kjt8tAplRvJpbSt4nbO1lugcT/kAaWzjlLQ==}
|
||||
'@next/env@15.2.1':
|
||||
resolution: {integrity: sha512-JmY0qvnPuS2NCWOz2bbby3Pe0VzdAQ7XpEB6uLIHmtXNfAsAO0KLQLkuAoc42Bxbo3/jMC3dcn9cdf+piCcG2Q==}
|
||||
|
||||
'@next/eslint-plugin-next@15.1.7':
|
||||
resolution: {integrity: sha512-kRP7RjSxfTO13NE317ek3mSGzoZlI33nc/i5hs1KaWpK+egs85xg0DJ4p32QEiHnR0mVjuUfhRIun7awqfL7pQ==}
|
||||
'@next/eslint-plugin-next@15.2.1':
|
||||
resolution: {integrity: sha512-6ppeToFd02z38SllzWxayLxjjNfzvc7Wm07gQOKSLjyASvKcXjNStZrLXMHuaWkhjqxe+cnhb2uzfWXm1VEj/Q==}
|
||||
|
||||
'@next/eslint-plugin-next@15.2.0':
|
||||
resolution: {integrity: sha512-jHFUG2OwmAuOASqq253RAEG/5BYcPHn27p1NoWZDCf4OdvdK0yRYWX92YKkL+Mk2s+GyJrmd/GATlL5b2IySpw==}
|
||||
|
||||
'@next/swc-darwin-arm64@15.1.7':
|
||||
resolution: {integrity: sha512-hPFwzPJDpA8FGj7IKV3Yf1web3oz2YsR8du4amKw8d+jAOHfYHYFpMkoF6vgSY4W6vB29RtZEklK9ayinGiCmQ==}
|
||||
'@next/swc-darwin-arm64@15.2.1':
|
||||
resolution: {integrity: sha512-aWXT+5KEREoy3K5AKtiKwioeblmOvFFjd+F3dVleLvvLiQ/mD//jOOuUcx5hzcO9ISSw4lrqtUPntTpK32uXXQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-darwin-x64@15.1.7':
|
||||
resolution: {integrity: sha512-2qoas+fO3OQKkU0PBUfwTiw/EYpN+kdAx62cePRyY1LqKtP09Vp5UcUntfZYajop5fDFTjSxCHfZVRxzi+9FYQ==}
|
||||
'@next/swc-darwin-x64@15.2.1':
|
||||
resolution: {integrity: sha512-E/w8ervu4fcG5SkLhvn1NE/2POuDCDEy5gFbfhmnYXkyONZR68qbUlJlZwuN82o7BrBVAw+tkR8nTIjGiMW1jQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-linux-arm64-gnu@15.1.7':
|
||||
resolution: {integrity: sha512-sKLLwDX709mPdzxMnRIXLIT9zaX2w0GUlkLYQnKGoXeWUhcvpCrK+yevcwCJPdTdxZEUA0mOXGLdPsGkudGdnA==}
|
||||
'@next/swc-linux-arm64-gnu@15.2.1':
|
||||
resolution: {integrity: sha512-gXDX5lIboebbjhiMT6kFgu4svQyjoSed6dHyjx5uZsjlvTwOAnZpn13w9XDaIMFFHw7K8CpBK7HfDKw0VZvUXQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-arm64-musl@15.1.7':
|
||||
resolution: {integrity: sha512-zblK1OQbQWdC8fxdX4fpsHDw+VSpBPGEUX4PhSE9hkaWPrWoeIJn+baX53vbsbDRaDKd7bBNcXRovY1hEhFd7w==}
|
||||
'@next/swc-linux-arm64-musl@15.2.1':
|
||||
resolution: {integrity: sha512-3v0pF/adKZkBWfUffmB/ROa+QcNTrnmYG4/SS+r52HPwAK479XcWoES2I+7F7lcbqc7mTeVXrIvb4h6rR/iDKg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-gnu@15.1.7':
|
||||
resolution: {integrity: sha512-GOzXutxuLvLHFDAPsMP2zDBMl1vfUHHpdNpFGhxu90jEzH6nNIgmtw/s1MDwpTOiM+MT5V8+I1hmVFeAUhkbgQ==}
|
||||
'@next/swc-linux-x64-gnu@15.2.1':
|
||||
resolution: {integrity: sha512-RbsVq2iB6KFJRZ2cHrU67jLVLKeuOIhnQB05ygu5fCNgg8oTewxweJE8XlLV+Ii6Y6u4EHwETdUiRNXIAfpBww==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-musl@15.1.7':
|
||||
resolution: {integrity: sha512-WrZ7jBhR7ATW1z5iEQ0ZJfE2twCNSXbpCSaAunF3BKcVeHFADSI/AW1y5Xt3DzTqPF1FzQlwQTewqetAABhZRQ==}
|
||||
'@next/swc-linux-x64-musl@15.2.1':
|
||||
resolution: {integrity: sha512-QHsMLAyAIu6/fWjHmkN/F78EFPKmhQlyX5C8pRIS2RwVA7z+t9cTb0IaYWC3EHLOTjsU7MNQW+n2xGXr11QPpg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@15.1.7':
|
||||
resolution: {integrity: sha512-LDnj1f3OVbou1BqvvXVqouJZKcwq++mV2F+oFHptToZtScIEnhNRJAhJzqAtTE2dB31qDYL45xJwrc+bLeKM2Q==}
|
||||
'@next/swc-win32-arm64-msvc@15.2.1':
|
||||
resolution: {integrity: sha512-Gk42XZXo1cE89i3hPLa/9KZ8OuupTjkDmhLaMKFohjf9brOeZVEa3BQy1J9s9TWUqPhgAEbwv6B2+ciGfe54Vw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-x64-msvc@15.1.7':
|
||||
resolution: {integrity: sha512-dC01f1quuf97viOfW05/K8XYv2iuBgAxJZl7mbCKEjMgdQl5JjAKJ0D2qMKZCgPWDeFbFT0Q0nYWwytEW0DWTQ==}
|
||||
'@next/swc-win32-x64-msvc@15.2.1':
|
||||
resolution: {integrity: sha512-YjqXCl8QGhVlMR8uBftWk0iTmvtntr41PhG1kvzGp0sUP/5ehTM+cwx25hKE54J0CRnHYjSGjSH3gkHEaHIN9g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@ -1653,8 +1650,8 @@ packages:
|
||||
'@types/json5@0.0.29':
|
||||
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
||||
|
||||
'@types/node@22.13.8':
|
||||
resolution: {integrity: sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==}
|
||||
'@types/node@22.13.9':
|
||||
resolution: {integrity: sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==}
|
||||
|
||||
'@types/pg@8.11.11':
|
||||
resolution: {integrity: sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw==}
|
||||
@ -1819,8 +1816,8 @@ packages:
|
||||
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
||||
engines: {node: '>= 6.0.0'}
|
||||
|
||||
ai@4.1.47:
|
||||
resolution: {integrity: sha512-9UZ8Mkv1HlprCJfQ0Kq+rgKbfkrkDtJjslr1WOHBRzvC70jDJJYp8r0Qq4vlF7zs9VBkGyy8Rhm5zQJJIUjvgw==}
|
||||
ai@4.1.50:
|
||||
resolution: {integrity: sha512-YBNeemrJKDrxoBQd3V9aaxhKm5q5YyRcF7PZE7W0NmLuvsdva/1aQNYTAsxs47gQFdvqfYmlFy4B0E+356OlPA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
@ -2351,8 +2348,8 @@ packages:
|
||||
engines: {node: '>=6.0'}
|
||||
hasBin: true
|
||||
|
||||
eslint-config-next@15.1.7:
|
||||
resolution: {integrity: sha512-zXoMnYUIy3XHaAoOhrcYkT9UQWvXqWju2K7NNsmb5wd/7XESDwof61eUdW4QhERr3eJ9Ko/vnXqIrj8kk/drYw==}
|
||||
eslint-config-next@15.2.1:
|
||||
resolution: {integrity: sha512-mhsprz7l0no8X+PdDnVHF4dZKu9YBJp2Rf6ztWbXBLJ4h6gxmW//owbbGJMBVUU+PibGJDAqZhW4pt8SC8HSow==}
|
||||
peerDependencies:
|
||||
eslint: ^7.23.0 || ^8.0.0 || ^9.0.0
|
||||
typescript: '>=3.3.1'
|
||||
@ -2557,8 +2554,8 @@ packages:
|
||||
resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
framer-motion@12.4.7:
|
||||
resolution: {integrity: sha512-VhrcbtcAMXfxlrjeHPpWVu2+mkcoR31e02aNSR7OUS/hZAciKa8q6o3YN2mA1h+jjscRsSyKvX6E1CiY/7OLMw==}
|
||||
framer-motion@12.4.10:
|
||||
resolution: {integrity: sha512-3Msuyjcr1Pb5hjkn4EJcRe1HumaveP0Gbv4DBMKTPKcV/1GSMkQXj+Uqgneys+9DPcZM18Hac9qY9iUEF5LZtg==}
|
||||
peerDependencies:
|
||||
'@emotion/is-prop-valid': '*'
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
@ -3288,11 +3285,11 @@ packages:
|
||||
minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
motion-dom@12.4.5:
|
||||
resolution: {integrity: sha512-Q2xmhuyYug1CGTo0jdsL05EQ4RhIYXlggFS/yPhQQRNzbrhjKQ1tbjThx5Plv68aX31LsUQRq4uIkuDxdO5vRQ==}
|
||||
motion-dom@12.4.10:
|
||||
resolution: {integrity: sha512-ISP5u6FTceoD6qKdLupIPU/LyXBrxGox+P2e3mBbm1+pLdlBbwv01YENJr7+1WZnW5ucVKzFScYsV1eXTCG4Xg==}
|
||||
|
||||
motion-utils@12.0.0:
|
||||
resolution: {integrity: sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA==}
|
||||
motion-utils@12.4.10:
|
||||
resolution: {integrity: sha512-NPwZd94V013SwRf++jMrk2+HEBgPkeIE2RiOzhAuuQlqxMJPkKt/LXVh6Upl+iN8oarSGD2dlY5/bqgsYXDABA==}
|
||||
|
||||
mrmime@2.0.1:
|
||||
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
|
||||
@ -3336,8 +3333,8 @@ packages:
|
||||
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
|
||||
next@15.1.7:
|
||||
resolution: {integrity: sha512-GNeINPGS9c6OZKCvKypbL8GTsT5GhWPp4DM0fzkXJuXMilOO2EeFxuAY6JZbtk6XIl6Ws10ag3xRINDjSO5+wg==}
|
||||
next@15.2.1:
|
||||
resolution: {integrity: sha512-zxbsdQv3OqWXybK5tMkPCBKyhIz63RstJ+NvlfkaLMc/m5MwXgz2e92k+hSKcyBpyADhMk2C31RIiaDjUZae7g==}
|
||||
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@ -4322,7 +4319,7 @@ snapshots:
|
||||
dependencies:
|
||||
json-schema: 0.4.0
|
||||
|
||||
'@ai-sdk/react@1.1.19(react@19.0.0)(zod@3.24.2)':
|
||||
'@ai-sdk/react@1.1.20(react@19.0.0)(zod@3.24.2)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider-utils': 2.1.10(zod@3.24.2)
|
||||
'@ai-sdk/ui-utils': 1.1.16(zod@3.24.2)
|
||||
@ -5197,27 +5194,27 @@ snapshots:
|
||||
'@jest/console@29.7.0':
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 22.13.8
|
||||
'@types/node': 22.13.9
|
||||
chalk: 4.1.2
|
||||
jest-message-util: 29.7.0
|
||||
jest-util: 29.7.0
|
||||
slash: 3.0.0
|
||||
|
||||
'@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2))':
|
||||
'@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.13.9)(typescript@5.8.2))':
|
||||
dependencies:
|
||||
'@jest/console': 29.7.0
|
||||
'@jest/reporters': 29.7.0
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/transform': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 22.13.8
|
||||
'@types/node': 22.13.9
|
||||
ansi-escapes: 4.3.2
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.9.0
|
||||
exit: 0.1.2
|
||||
graceful-fs: 4.2.11
|
||||
jest-changed-files: 29.7.0
|
||||
jest-config: 29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2))
|
||||
jest-config: 29.7.0(@types/node@22.13.9)(ts-node@10.9.2(@types/node@22.13.9)(typescript@5.8.2))
|
||||
jest-haste-map: 29.7.0
|
||||
jest-message-util: 29.7.0
|
||||
jest-regex-util: 29.6.3
|
||||
@ -5242,7 +5239,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@jest/fake-timers': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 22.13.8
|
||||
'@types/node': 22.13.9
|
||||
jest-mock: 29.7.0
|
||||
|
||||
'@jest/expect-utils@29.7.0':
|
||||
@ -5260,7 +5257,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
'@sinonjs/fake-timers': 10.3.0
|
||||
'@types/node': 22.13.8
|
||||
'@types/node': 22.13.9
|
||||
jest-message-util: 29.7.0
|
||||
jest-mock: 29.7.0
|
||||
jest-util: 29.7.0
|
||||
@ -5282,7 +5279,7 @@ snapshots:
|
||||
'@jest/transform': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
'@types/node': 22.13.8
|
||||
'@types/node': 22.13.9
|
||||
chalk: 4.1.2
|
||||
collect-v8-coverage: 1.0.2
|
||||
exit: 0.1.2
|
||||
@ -5352,7 +5349,7 @@ snapshots:
|
||||
'@jest/schemas': 29.6.3
|
||||
'@types/istanbul-lib-coverage': 2.0.6
|
||||
'@types/istanbul-reports': 3.0.4
|
||||
'@types/node': 22.13.8
|
||||
'@types/node': 22.13.9
|
||||
'@types/yargs': 17.0.33
|
||||
chalk: 4.1.2
|
||||
|
||||
@ -5378,45 +5375,41 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
||||
'@next/bundle-analyzer@15.1.7':
|
||||
'@next/bundle-analyzer@15.2.1':
|
||||
dependencies:
|
||||
webpack-bundle-analyzer: 4.10.1
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
'@next/env@15.1.7': {}
|
||||
'@next/env@15.2.1': {}
|
||||
|
||||
'@next/eslint-plugin-next@15.1.7':
|
||||
'@next/eslint-plugin-next@15.2.1':
|
||||
dependencies:
|
||||
fast-glob: 3.3.1
|
||||
|
||||
'@next/eslint-plugin-next@15.2.0':
|
||||
dependencies:
|
||||
fast-glob: 3.3.1
|
||||
|
||||
'@next/swc-darwin-arm64@15.1.7':
|
||||
'@next/swc-darwin-arm64@15.2.1':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-x64@15.1.7':
|
||||
'@next/swc-darwin-x64@15.2.1':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-gnu@15.1.7':
|
||||
'@next/swc-linux-arm64-gnu@15.2.1':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-musl@15.1.7':
|
||||
'@next/swc-linux-arm64-musl@15.2.1':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-gnu@15.1.7':
|
||||
'@next/swc-linux-x64-gnu@15.2.1':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-musl@15.1.7':
|
||||
'@next/swc-linux-x64-musl@15.2.1':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-arm64-msvc@15.1.7':
|
||||
'@next/swc-win32-arm64-msvc@15.2.1':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-x64-msvc@15.1.7':
|
||||
'@next/swc-win32-x64-msvc@15.2.1':
|
||||
optional: true
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
@ -6216,7 +6209,7 @@ snapshots:
|
||||
|
||||
'@types/graceful-fs@4.1.9':
|
||||
dependencies:
|
||||
'@types/node': 22.13.8
|
||||
'@types/node': 22.13.9
|
||||
|
||||
'@types/istanbul-lib-coverage@2.0.6': {}
|
||||
|
||||
@ -6235,7 +6228,7 @@ snapshots:
|
||||
|
||||
'@types/jsdom@20.0.1':
|
||||
dependencies:
|
||||
'@types/node': 22.13.8
|
||||
'@types/node': 22.13.9
|
||||
'@types/tough-cookie': 4.0.5
|
||||
parse5: 7.2.1
|
||||
|
||||
@ -6243,13 +6236,13 @@ snapshots:
|
||||
|
||||
'@types/json5@0.0.29': {}
|
||||
|
||||
'@types/node@22.13.8':
|
||||
'@types/node@22.13.9':
|
||||
dependencies:
|
||||
undici-types: 6.20.0
|
||||
|
||||
'@types/pg@8.11.11':
|
||||
dependencies:
|
||||
'@types/node': 22.13.8
|
||||
'@types/node': 22.13.9
|
||||
pg-protocol: 1.7.1
|
||||
pg-types: 4.0.2
|
||||
|
||||
@ -6365,9 +6358,9 @@ snapshots:
|
||||
dependencies:
|
||||
crypto-js: 4.2.0
|
||||
|
||||
'@vercel/analytics@1.5.0(next@15.1.7(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)':
|
||||
'@vercel/analytics@1.5.0(next@15.2.1(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)':
|
||||
optionalDependencies:
|
||||
next: 15.1.7(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
next: 15.2.1(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
react: 19.0.0
|
||||
|
||||
'@vercel/blob@0.27.2':
|
||||
@ -6378,9 +6371,9 @@ snapshots:
|
||||
throttleit: 2.1.0
|
||||
undici: 5.28.5
|
||||
|
||||
'@vercel/speed-insights@1.2.0(next@15.1.7(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)':
|
||||
'@vercel/speed-insights@1.2.0(next@15.2.1(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)':
|
||||
optionalDependencies:
|
||||
next: 15.1.7(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
next: 15.2.1(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
react: 19.0.0
|
||||
|
||||
abab@2.0.6: {}
|
||||
@ -6406,11 +6399,11 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
ai@4.1.47(react@19.0.0)(zod@3.24.2):
|
||||
ai@4.1.50(react@19.0.0)(zod@3.24.2):
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.0.9
|
||||
'@ai-sdk/provider-utils': 2.1.10(zod@3.24.2)
|
||||
'@ai-sdk/react': 1.1.19(react@19.0.0)(zod@3.24.2)
|
||||
'@ai-sdk/react': 1.1.20(react@19.0.0)(zod@3.24.2)
|
||||
'@ai-sdk/ui-utils': 1.1.16(zod@3.24.2)
|
||||
'@opentelemetry/api': 1.9.0
|
||||
jsondiffpatch: 0.6.0
|
||||
@ -6737,13 +6730,13 @@ snapshots:
|
||||
|
||||
cookie@0.7.1: {}
|
||||
|
||||
create-jest@29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2)):
|
||||
create-jest@29.7.0(@types/node@22.13.9)(ts-node@10.9.2(@types/node@22.13.9)(typescript@5.8.2)):
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
chalk: 4.1.2
|
||||
exit: 0.1.2
|
||||
graceful-fs: 4.2.11
|
||||
jest-config: 29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2))
|
||||
jest-config: 29.7.0(@types/node@22.13.9)(ts-node@10.9.2(@types/node@22.13.9)(typescript@5.8.2))
|
||||
jest-util: 29.7.0
|
||||
prompts: 2.4.2
|
||||
transitivePeerDependencies:
|
||||
@ -7029,9 +7022,9 @@ snapshots:
|
||||
optionalDependencies:
|
||||
source-map: 0.6.1
|
||||
|
||||
eslint-config-next@15.1.7(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2):
|
||||
eslint-config-next@15.2.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2):
|
||||
dependencies:
|
||||
'@next/eslint-plugin-next': 15.1.7
|
||||
'@next/eslint-plugin-next': 15.2.1
|
||||
'@rushstack/eslint-patch': 1.10.5
|
||||
'@typescript-eslint/eslint-plugin': 8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2)
|
||||
'@typescript-eslint/parser': 8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2)
|
||||
@ -7327,10 +7320,10 @@ snapshots:
|
||||
es-set-tostringtag: 2.1.0
|
||||
mime-types: 2.1.35
|
||||
|
||||
framer-motion@12.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||
framer-motion@12.4.10(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||
dependencies:
|
||||
motion-dom: 12.4.5
|
||||
motion-utils: 12.0.0
|
||||
motion-dom: 12.4.10
|
||||
motion-utils: 12.4.10
|
||||
tslib: 2.8.1
|
||||
optionalDependencies:
|
||||
react: 19.0.0
|
||||
@ -7706,7 +7699,7 @@ snapshots:
|
||||
'@jest/expect': 29.7.0
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 22.13.8
|
||||
'@types/node': 22.13.9
|
||||
chalk: 4.1.2
|
||||
co: 4.6.0
|
||||
dedent: 1.5.3
|
||||
@ -7726,16 +7719,16 @@ snapshots:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
|
||||
jest-cli@29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2)):
|
||||
jest-cli@29.7.0(@types/node@22.13.9)(ts-node@10.9.2(@types/node@22.13.9)(typescript@5.8.2)):
|
||||
dependencies:
|
||||
'@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2))
|
||||
'@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.13.9)(typescript@5.8.2))
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
chalk: 4.1.2
|
||||
create-jest: 29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2))
|
||||
create-jest: 29.7.0(@types/node@22.13.9)(ts-node@10.9.2(@types/node@22.13.9)(typescript@5.8.2))
|
||||
exit: 0.1.2
|
||||
import-local: 3.2.0
|
||||
jest-config: 29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2))
|
||||
jest-config: 29.7.0(@types/node@22.13.9)(ts-node@10.9.2(@types/node@22.13.9)(typescript@5.8.2))
|
||||
jest-util: 29.7.0
|
||||
jest-validate: 29.7.0
|
||||
yargs: 17.7.2
|
||||
@ -7745,7 +7738,7 @@ snapshots:
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
jest-config@29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2)):
|
||||
jest-config@29.7.0(@types/node@22.13.9)(ts-node@10.9.2(@types/node@22.13.9)(typescript@5.8.2)):
|
||||
dependencies:
|
||||
'@babel/core': 7.26.9
|
||||
'@jest/test-sequencer': 29.7.0
|
||||
@ -7770,8 +7763,8 @@ snapshots:
|
||||
slash: 3.0.0
|
||||
strip-json-comments: 3.1.1
|
||||
optionalDependencies:
|
||||
'@types/node': 22.13.8
|
||||
ts-node: 10.9.2(@types/node@22.13.8)(typescript@5.8.2)
|
||||
'@types/node': 22.13.9
|
||||
ts-node: 10.9.2(@types/node@22.13.9)(typescript@5.8.2)
|
||||
transitivePeerDependencies:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
@ -7801,7 +7794,7 @@ snapshots:
|
||||
'@jest/fake-timers': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@types/jsdom': 20.0.1
|
||||
'@types/node': 22.13.8
|
||||
'@types/node': 22.13.9
|
||||
jest-mock: 29.7.0
|
||||
jest-util: 29.7.0
|
||||
jsdom: 20.0.3
|
||||
@ -7815,7 +7808,7 @@ snapshots:
|
||||
'@jest/environment': 29.7.0
|
||||
'@jest/fake-timers': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 22.13.8
|
||||
'@types/node': 22.13.9
|
||||
jest-mock: 29.7.0
|
||||
jest-util: 29.7.0
|
||||
|
||||
@ -7825,7 +7818,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
'@types/graceful-fs': 4.1.9
|
||||
'@types/node': 22.13.8
|
||||
'@types/node': 22.13.9
|
||||
anymatch: 3.1.3
|
||||
fb-watchman: 2.0.2
|
||||
graceful-fs: 4.2.11
|
||||
@ -7864,7 +7857,7 @@ snapshots:
|
||||
jest-mock@29.7.0:
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 22.13.8
|
||||
'@types/node': 22.13.9
|
||||
jest-util: 29.7.0
|
||||
|
||||
jest-pnp-resolver@1.2.3(jest-resolve@29.7.0):
|
||||
@ -7899,7 +7892,7 @@ snapshots:
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/transform': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 22.13.8
|
||||
'@types/node': 22.13.9
|
||||
chalk: 4.1.2
|
||||
emittery: 0.13.1
|
||||
graceful-fs: 4.2.11
|
||||
@ -7927,7 +7920,7 @@ snapshots:
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/transform': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 22.13.8
|
||||
'@types/node': 22.13.9
|
||||
chalk: 4.1.2
|
||||
cjs-module-lexer: 1.4.3
|
||||
collect-v8-coverage: 1.0.2
|
||||
@ -7973,7 +7966,7 @@ snapshots:
|
||||
jest-util@29.7.0:
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 22.13.8
|
||||
'@types/node': 22.13.9
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.9.0
|
||||
graceful-fs: 4.2.11
|
||||
@ -7992,7 +7985,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 22.13.8
|
||||
'@types/node': 22.13.9
|
||||
ansi-escapes: 4.3.2
|
||||
chalk: 4.1.2
|
||||
emittery: 0.13.1
|
||||
@ -8001,17 +7994,17 @@ snapshots:
|
||||
|
||||
jest-worker@29.7.0:
|
||||
dependencies:
|
||||
'@types/node': 22.13.8
|
||||
'@types/node': 22.13.9
|
||||
jest-util: 29.7.0
|
||||
merge-stream: 2.0.0
|
||||
supports-color: 8.1.1
|
||||
|
||||
jest@29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2)):
|
||||
jest@29.7.0(@types/node@22.13.9)(ts-node@10.9.2(@types/node@22.13.9)(typescript@5.8.2)):
|
||||
dependencies:
|
||||
'@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2))
|
||||
'@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.13.9)(typescript@5.8.2))
|
||||
'@jest/types': 29.6.3
|
||||
import-local: 3.2.0
|
||||
jest-cli: 29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2))
|
||||
jest-cli: 29.7.0(@types/node@22.13.9)(ts-node@10.9.2(@types/node@22.13.9)(typescript@5.8.2))
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- babel-plugin-macros
|
||||
@ -8230,11 +8223,11 @@ snapshots:
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
motion-dom@12.4.5:
|
||||
motion-dom@12.4.10:
|
||||
dependencies:
|
||||
motion-utils: 12.0.0
|
||||
motion-utils: 12.4.10
|
||||
|
||||
motion-utils@12.0.0: {}
|
||||
motion-utils@12.4.10: {}
|
||||
|
||||
mrmime@2.0.1: {}
|
||||
|
||||
@ -8246,10 +8239,10 @@ snapshots:
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
next-auth@5.0.0-beta.25(next@15.1.7(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0):
|
||||
next-auth@5.0.0-beta.25(next@15.2.1(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0):
|
||||
dependencies:
|
||||
'@auth/core': 0.37.2
|
||||
next: 15.1.7(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
next: 15.2.1(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
react: 19.0.0
|
||||
|
||||
next-themes@0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||
@ -8257,9 +8250,9 @@ snapshots:
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
|
||||
next@15.1.7(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||
next@15.2.1(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||
dependencies:
|
||||
'@next/env': 15.1.7
|
||||
'@next/env': 15.2.1
|
||||
'@swc/counter': 0.1.3
|
||||
'@swc/helpers': 0.5.15
|
||||
busboy: 1.6.0
|
||||
@ -8269,14 +8262,14 @@ snapshots:
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
styled-jsx: 5.1.6(@babel/core@7.26.9)(react@19.0.0)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 15.1.7
|
||||
'@next/swc-darwin-x64': 15.1.7
|
||||
'@next/swc-linux-arm64-gnu': 15.1.7
|
||||
'@next/swc-linux-arm64-musl': 15.1.7
|
||||
'@next/swc-linux-x64-gnu': 15.1.7
|
||||
'@next/swc-linux-x64-musl': 15.1.7
|
||||
'@next/swc-win32-arm64-msvc': 15.1.7
|
||||
'@next/swc-win32-x64-msvc': 15.1.7
|
||||
'@next/swc-darwin-arm64': 15.2.1
|
||||
'@next/swc-darwin-x64': 15.2.1
|
||||
'@next/swc-linux-arm64-gnu': 15.2.1
|
||||
'@next/swc-linux-arm64-musl': 15.2.1
|
||||
'@next/swc-linux-x64-gnu': 15.2.1
|
||||
'@next/swc-linux-x64-musl': 15.2.1
|
||||
'@next/swc-win32-arm64-msvc': 15.2.1
|
||||
'@next/swc-win32-x64-msvc': 15.2.1
|
||||
'@opentelemetry/api': 1.9.0
|
||||
sharp: 0.33.5
|
||||
transitivePeerDependencies:
|
||||
@ -8980,14 +8973,14 @@ snapshots:
|
||||
dependencies:
|
||||
sax: 1.2.4
|
||||
|
||||
ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2):
|
||||
ts-node@10.9.2(@types/node@22.13.9)(typescript@5.8.2):
|
||||
dependencies:
|
||||
'@cspotcode/source-map-support': 0.8.1
|
||||
'@tsconfig/node10': 1.0.11
|
||||
'@tsconfig/node12': 1.0.11
|
||||
'@tsconfig/node14': 1.0.3
|
||||
'@tsconfig/node16': 1.0.4
|
||||
'@types/node': 22.13.8
|
||||
'@types/node': 22.13.9
|
||||
acorn: 8.14.0
|
||||
acorn-walk: 8.3.4
|
||||
arg: 4.1.3
|
||||
|
||||
@ -24,6 +24,7 @@ export const PREFIX_TAG = '/tag';
|
||||
export const PREFIX_CAMERA = '/shot-on';
|
||||
export const PREFIX_FILM_SIMULATION = '/film';
|
||||
export const PREFIX_FOCAL_LENGTH = '/focal';
|
||||
export const PREFIX_RECIPE = '/recipe';
|
||||
|
||||
// Dynamic paths
|
||||
const PATH_PHOTO_DYNAMIC = `${PREFIX_PHOTO}/[photoId]`;
|
||||
@ -32,10 +33,7 @@ const PATH_CAMERA_DYNAMIC = `${PREFIX_CAMERA}/[make]/[model]`;
|
||||
// eslint-disable-next-line max-len
|
||||
const PATH_FILM_SIMULATION_DYNAMIC = `${PREFIX_FILM_SIMULATION}/[simulation]`;
|
||||
const PATH_FOCAL_LENGTH_DYNAMIC = `${PREFIX_FOCAL_LENGTH}/[focal]`;
|
||||
|
||||
// Search params
|
||||
export const SEARCH_PARAM_SHOW = 'show';
|
||||
export const SEARCH_PARAM_SHOW_RECIPE = 'recipe';
|
||||
const PATH_RECIPE_DYNAMIC = `${PREFIX_RECIPE}/[recipe]`;
|
||||
|
||||
// Admin paths
|
||||
export const PATH_ADMIN_PHOTOS = `${PATH_ADMIN}/photos`;
|
||||
@ -80,6 +78,7 @@ export const PATHS_TO_CACHE = [
|
||||
PATH_CAMERA_DYNAMIC,
|
||||
PATH_FILM_SIMULATION_DYNAMIC,
|
||||
PATH_FOCAL_LENGTH_DYNAMIC,
|
||||
PATH_RECIPE_DYNAMIC,
|
||||
...PATHS_ADMIN,
|
||||
];
|
||||
|
||||
@ -110,9 +109,9 @@ export const pathForPhoto = ({
|
||||
camera,
|
||||
simulation,
|
||||
focal,
|
||||
showRecipe,
|
||||
}: PhotoPathParams) => {
|
||||
const path = typeof photo !== 'string' && photo.hidden
|
||||
recipe,
|
||||
}: PhotoPathParams) =>
|
||||
typeof photo !== 'string' && photo.hidden
|
||||
? `${pathForTag(TAG_HIDDEN)}/${getPhotoId(photo)}`
|
||||
: tag
|
||||
? `${pathForTag(tag)}/${getPhotoId(photo)}`
|
||||
@ -122,11 +121,9 @@ export const pathForPhoto = ({
|
||||
? `${pathForFilmSimulation(simulation)}/${getPhotoId(photo)}`
|
||||
: focal
|
||||
? `${pathForFocalLength(focal)}/${getPhotoId(photo)}`
|
||||
: `${PREFIX_PHOTO}/${getPhotoId(photo)}`;
|
||||
return showRecipe
|
||||
? `${path}?${SEARCH_PARAM_SHOW}=${SEARCH_PARAM_SHOW_RECIPE}`
|
||||
: path;
|
||||
};
|
||||
: recipe
|
||||
? `${pathForRecipe(recipe)}/${getPhotoId(photo)}`
|
||||
: `${PREFIX_PHOTO}/${getPhotoId(photo)}`;
|
||||
|
||||
export const pathForTag = (tag: string) =>
|
||||
`${PREFIX_TAG}/${tag}`;
|
||||
@ -140,6 +137,9 @@ export const pathForFilmSimulation = (simulation: FilmSimulation) =>
|
||||
export const pathForFocalLength = (focal: number) =>
|
||||
`${PREFIX_FOCAL_LENGTH}/${focal}mm`;
|
||||
|
||||
export const pathForRecipe = (recipe: string) =>
|
||||
`${PREFIX_RECIPE}/${recipe}`;
|
||||
|
||||
export const absolutePathForPhoto = (params: PhotoPathParams) =>
|
||||
`${BASE_URL}${pathForPhoto(params)}`;
|
||||
|
||||
@ -152,6 +152,9 @@ export const absolutePathForCamera= (camera: Camera) =>
|
||||
export const absolutePathForFilmSimulation = (simulation: FilmSimulation) =>
|
||||
`${BASE_URL}${pathForFilmSimulation(simulation)}`;
|
||||
|
||||
export const absolutePathForRecipe = (recipe: string) =>
|
||||
`${BASE_URL}${pathForRecipe(recipe)}`;
|
||||
|
||||
export const absolutePathForFocalLength = (focal: number) =>
|
||||
`${BASE_URL}${pathForFocalLength(focal)}`;
|
||||
|
||||
@ -168,6 +171,9 @@ export const absolutePathForFilmSimulationImage =
|
||||
(simulation: FilmSimulation) =>
|
||||
`${absolutePathForFilmSimulation(simulation)}/image`;
|
||||
|
||||
export const absolutePathForRecipeImage = (recipe: string) =>
|
||||
`${absolutePathForRecipe(recipe)}/image`;
|
||||
|
||||
export const absolutePathForFocalLengthImage =
|
||||
(focal: number) =>
|
||||
`${absolutePathForFocalLength(focal)}/image`;
|
||||
|
||||
@ -14,6 +14,7 @@ export default function PhotoCamera({
|
||||
contrast,
|
||||
prefetch,
|
||||
countOnHover,
|
||||
className,
|
||||
}: {
|
||||
camera: Camera
|
||||
hideAppleIcon?: boolean
|
||||
@ -37,6 +38,7 @@ export default function PhotoCamera({
|
||||
className="translate-x-[-0.5px]"
|
||||
/>}
|
||||
type={type}
|
||||
className={className}
|
||||
badged={badged}
|
||||
contrast={contrast}
|
||||
prefetch={prefetch}
|
||||
|
||||
@ -33,7 +33,7 @@ export default function HeaderList({
|
||||
)}
|
||||
>
|
||||
{icon &&
|
||||
<span className="w-[1rem]">
|
||||
<span className="text-icon w-[1rem]">
|
||||
{icon}
|
||||
</span>}
|
||||
{title}
|
||||
|
||||
@ -16,6 +16,7 @@ export default function Modal({
|
||||
onClose,
|
||||
className,
|
||||
anchor = 'center',
|
||||
container = true,
|
||||
children,
|
||||
fast,
|
||||
}: {
|
||||
@ -23,6 +24,7 @@ export default function Modal({
|
||||
onClose?: () => void
|
||||
className?: string
|
||||
anchor?: 'top' | 'center'
|
||||
container?: boolean
|
||||
children: ReactNode
|
||||
fast?: boolean
|
||||
}) {
|
||||
@ -80,11 +82,11 @@ export default function Modal({
|
||||
ref={contentRef}
|
||||
key="modalContent"
|
||||
className={clsx(
|
||||
'w-[calc(100vw-1.5rem)] sm:w-[min(540px,90vw)]',
|
||||
'p-3 rounded-lg',
|
||||
'md:p-4 md:rounded-xl',
|
||||
container && 'w-[calc(100vw-1.5rem)] sm:w-[min(540px,90vw)]',
|
||||
container && 'p-3 rounded-lg',
|
||||
container && 'md:p-4 md:rounded-xl',
|
||||
container && 'dark:border dark:border-gray-800',
|
||||
'bg-white dark:bg-black',
|
||||
'dark:border dark:border-gray-800',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@ -12,6 +12,7 @@ export interface EntityLinkExternalProps {
|
||||
badged?: boolean
|
||||
contrast?: ComponentProps<typeof Badge>['contrast']
|
||||
prefetch?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function EntityLink({
|
||||
@ -65,13 +66,13 @@ export default function EntityLink({
|
||||
</>;
|
||||
|
||||
return (
|
||||
<span className="group inline-flex w-full">
|
||||
<span className={clsx(
|
||||
'group inline-flex max-w-full overflow-hidden',
|
||||
className,
|
||||
)}>
|
||||
<LinkWithStatus
|
||||
href={href}
|
||||
className={clsx(
|
||||
'inline-flex items-center gap-2',
|
||||
className,
|
||||
)}
|
||||
className="inline-flex items-center gap-2 max-w-full"
|
||||
>
|
||||
{({ isLoading }) => <>
|
||||
<LabeledIcon {...{
|
||||
|
||||
@ -12,6 +12,7 @@ export default function PhotoFocalLength({
|
||||
contrast,
|
||||
prefetch,
|
||||
countOnHover,
|
||||
className,
|
||||
}: {
|
||||
focal: number
|
||||
countOnHover?: number
|
||||
@ -22,6 +23,7 @@ export default function PhotoFocalLength({
|
||||
href={pathForFocalLength(focal)}
|
||||
icon={<TbCone className="rotate-[270deg]" />}
|
||||
type={type}
|
||||
className={className}
|
||||
badged={badged}
|
||||
contrast={contrast}
|
||||
prefetch={prefetch}
|
||||
|
||||
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 * .003}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}
|
||||
/>,
|
||||
]}
|
||||
|
||||
@ -16,6 +16,7 @@ export default function PhotoGrid({
|
||||
camera,
|
||||
simulation,
|
||||
focal,
|
||||
recipe,
|
||||
photoPriority,
|
||||
fast,
|
||||
animate = true,
|
||||
@ -94,6 +95,7 @@ export default function PhotoGrid({
|
||||
camera,
|
||||
simulation,
|
||||
focal,
|
||||
recipe,
|
||||
selected: photo.id === selectedPhoto?.id,
|
||||
priority: photoPriority,
|
||||
onVisible: index === photos.length - 1
|
||||
|
||||
@ -15,6 +15,7 @@ export default function PhotoGridContainer({
|
||||
camera,
|
||||
simulation,
|
||||
focal,
|
||||
recipe,
|
||||
animateOnFirstLoadOnly,
|
||||
header,
|
||||
sidebar,
|
||||
@ -53,6 +54,7 @@ export default function PhotoGridContainer({
|
||||
camera,
|
||||
simulation,
|
||||
focal,
|
||||
recipe,
|
||||
animateOnFirstLoadOnly,
|
||||
onAnimationComplete,
|
||||
canSelect,
|
||||
@ -66,6 +68,7 @@ export default function PhotoGridContainer({
|
||||
camera,
|
||||
simulation,
|
||||
focal,
|
||||
recipe,
|
||||
animateOnFirstLoadOnly,
|
||||
canSelect,
|
||||
}} />}
|
||||
|
||||
@ -10,6 +10,7 @@ import PhotoGridContainer from './PhotoGridContainer';
|
||||
import { useEffect } from 'react';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import clsx from 'clsx/lite';
|
||||
import { Recipes } from '@/recipe';
|
||||
|
||||
export default function PhotoGridPage({
|
||||
photos,
|
||||
@ -17,12 +18,14 @@ export default function PhotoGridPage({
|
||||
tags,
|
||||
cameras,
|
||||
simulations,
|
||||
recipes,
|
||||
}: {
|
||||
photos: Photo[]
|
||||
photosCount: number
|
||||
tags: Tags
|
||||
cameras: Cameras
|
||||
simulations: FilmSimulations
|
||||
recipes: Recipes
|
||||
}) {
|
||||
const { setSelectedPhotoIds } = useAppState();
|
||||
|
||||
@ -63,6 +66,7 @@ export default function PhotoGridPage({
|
||||
tags,
|
||||
cameras,
|
||||
simulations,
|
||||
recipes,
|
||||
photosCount,
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -21,17 +21,22 @@ import {
|
||||
safelyParseFormattedHtml,
|
||||
} from '@/utility/html';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { Recipes, sortRecipesWithCount } from '@/recipe';
|
||||
import PhotoRecipe from '@/recipe/PhotoRecipe';
|
||||
import { TbChecklist } from 'react-icons/tb';
|
||||
|
||||
export default function PhotoGridSidebar({
|
||||
tags,
|
||||
cameras,
|
||||
simulations,
|
||||
recipes,
|
||||
photosCount,
|
||||
photosDateRange,
|
||||
}: {
|
||||
tags: Tags
|
||||
cameras: Cameras
|
||||
simulations: FilmSimulations
|
||||
recipes: Recipes
|
||||
photosCount: number
|
||||
photosDateRange?: PhotoDateRange
|
||||
}) {
|
||||
@ -48,7 +53,7 @@ export default function PhotoGridSidebar({
|
||||
title='Tags'
|
||||
icon={<FaTag
|
||||
size={12}
|
||||
className="text-icon translate-y-[1px]"
|
||||
className="translate-y-[1px]"
|
||||
/>}
|
||||
items={tagsIncludingHidden.map(({ tag, count }) => {
|
||||
switch (tag) {
|
||||
@ -90,7 +95,7 @@ export default function PhotoGridSidebar({
|
||||
title="Cameras"
|
||||
icon={<IoMdCamera
|
||||
size={13}
|
||||
className="text-icon translate-y-[-0.25px]"
|
||||
className="translate-y-[-0.25px]"
|
||||
/>}
|
||||
items={cameras
|
||||
.sort(sortCamerasWithCount)
|
||||
@ -108,6 +113,28 @@ export default function PhotoGridSidebar({
|
||||
/>
|
||||
: null;
|
||||
|
||||
const recipesContent = recipes.length > 0
|
||||
? <HeaderList
|
||||
title="Recipes"
|
||||
icon={<TbChecklist
|
||||
size={16}
|
||||
className="translate-x-[-1px]"
|
||||
/>}
|
||||
items={recipes
|
||||
.sort(sortRecipesWithCount)
|
||||
.map(({ recipe, count }) =>
|
||||
<PhotoRecipe
|
||||
key={recipe}
|
||||
recipe={recipe}
|
||||
type="text-only"
|
||||
countOnHover={count}
|
||||
prefetch={false}
|
||||
contrast="low"
|
||||
badged
|
||||
/>)}
|
||||
/>
|
||||
: null;
|
||||
|
||||
const filmsContent = simulations.length > 0
|
||||
? <HeaderList
|
||||
title="Films"
|
||||
@ -161,6 +188,7 @@ export default function PhotoGridSidebar({
|
||||
{SHOW_SIDEBAR_CAMERAS_FIRST
|
||||
? <>{camerasContent}{tagsContent}</>
|
||||
: <>{tagsContent}{camerasContent}</>}
|
||||
{recipesContent}
|
||||
{filmsContent}
|
||||
{photoStatsContent}
|
||||
</div>
|
||||
|
||||
@ -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 = () =>
|
||||
@ -149,6 +151,7 @@ export default function PhotoHeader({
|
||||
tag={tag}
|
||||
camera={camera}
|
||||
simulation={simulation}
|
||||
recipe={recipe}
|
||||
focal={focal}
|
||||
count={count}
|
||||
dateRange={dateRange}
|
||||
|
||||
@ -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';
|
||||
@ -38,11 +38,12 @@ import { LuExpand } from 'react-icons/lu';
|
||||
import LoaderButton from '@/components/primitives/LoaderButton';
|
||||
import Tooltip from '@/components/Tooltip';
|
||||
import ZoomControls, { ZoomControlsRef } from '@/components/image/ZoomControls';
|
||||
import PhotoRecipe from './PhotoRecipe';
|
||||
import { TbChecklist } from 'react-icons/tb';
|
||||
import { IoCloseSharp } from 'react-icons/io5';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import useRecipeState from './useRecipeState';
|
||||
import useRecipeState from '../recipe/useRecipeState';
|
||||
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,25 @@ export default function PhotoLarge({
|
||||
const showZoomControls = showZoomControlsProp && areZoomControlsShown;
|
||||
|
||||
const refRecipe = useRef<HTMLDivElement>(null);
|
||||
const refRecipeTrigger = useRef<HTMLButtonElement>(null);
|
||||
const refRecipeButton = useRef<HTMLButtonElement>(null);
|
||||
const refTriggers = useMemo(() => [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 +141,7 @@ export default function PhotoLarge({
|
||||
const hasMetaContent =
|
||||
showCameraContent ||
|
||||
showTagsContent ||
|
||||
showRecipeContent ||
|
||||
showExifContent;
|
||||
|
||||
const hasNonDateContent =
|
||||
@ -185,11 +195,11 @@ export default function PhotoLarge({
|
||||
)}>
|
||||
<AnimatePresence>
|
||||
{(shouldShowRecipe || shouldDebugRecipeOverlays) &&
|
||||
photo.fujifilmRecipe &&
|
||||
photo.recipeData &&
|
||||
photo.filmSimulation &&
|
||||
<PhotoRecipe
|
||||
<PhotoRecipeOverlay
|
||||
ref={refRecipe}
|
||||
recipe={photo.fujifilmRecipe}
|
||||
recipe={photo.recipeData}
|
||||
simulation={photo.filmSimulation}
|
||||
iso={photo.isoFormatted}
|
||||
exposure={photo.exposureCompensationFormatted}
|
||||
@ -250,7 +260,7 @@ export default function PhotoLarge({
|
||||
)}>
|
||||
{photo.caption}
|
||||
</div>}
|
||||
{(showCameraContent || showTagsContent) &&
|
||||
{(showCameraContent || showRecipeContent || showTagsContent) &&
|
||||
<div>
|
||||
{showCameraContent &&
|
||||
<PhotoCamera
|
||||
@ -258,6 +268,12 @@ export default function PhotoLarge({
|
||||
contrast="medium"
|
||||
prefetch={prefetchRelatedLinks}
|
||||
/>}
|
||||
{showRecipeContent &&
|
||||
<PhotoRecipe
|
||||
recipe={recipe}
|
||||
contrast="medium"
|
||||
prefetch={prefetchRelatedLinks}
|
||||
/>}
|
||||
{showTagsContent &&
|
||||
<PhotoTags
|
||||
tags={tags}
|
||||
@ -270,7 +286,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 +326,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 +334,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 +393,9 @@ export default function PhotoLarge({
|
||||
simulation={shouldShareSimulation
|
||||
? photo.filmSimulation
|
||||
: undefined}
|
||||
recipe={shouldShareRecipe
|
||||
? recipe
|
||||
: undefined}
|
||||
focal={shouldShareFocalLength
|
||||
? photo.focalLength
|
||||
: undefined}
|
||||
|
||||
@ -14,6 +14,7 @@ export default function PhotoLink({
|
||||
camera,
|
||||
simulation,
|
||||
focal,
|
||||
recipe,
|
||||
scroll,
|
||||
prefetch,
|
||||
nextPhotoAnimation,
|
||||
@ -32,7 +33,7 @@ export default function PhotoLink({
|
||||
return (
|
||||
photo
|
||||
? <Link
|
||||
href={pathForPhoto({ photo, tag, camera, simulation, focal })}
|
||||
href={pathForPhoto({ photo, tag, camera, simulation, focal, recipe })}
|
||||
prefetch={prefetch}
|
||||
onClick={() => {
|
||||
if (nextPhotoAnimation) {
|
||||
|
||||
@ -21,6 +21,7 @@ export default function PhotoMedium({
|
||||
camera,
|
||||
simulation,
|
||||
focal,
|
||||
recipe,
|
||||
selected,
|
||||
priority,
|
||||
prefetch = SHOULD_PREFETCH_ALL_LINKS,
|
||||
@ -41,7 +42,7 @@ export default function PhotoMedium({
|
||||
return (
|
||||
<LinkWithStatus
|
||||
ref={ref}
|
||||
href={pathForPhoto({ photo, tag, camera, simulation, focal })}
|
||||
href={pathForPhoto({ photo, tag, camera, simulation, focal, recipe })}
|
||||
className={clsx(
|
||||
'active:brightness-75',
|
||||
selected && 'brightness-50',
|
||||
@ -65,7 +66,7 @@ export default function PhotoMedium({
|
||||
aspectRatio={photo.aspectRatio}
|
||||
blurDataURL={photo.blurData}
|
||||
blurCompatibilityMode={doesPhotoNeedBlurCompatibility(photo)}
|
||||
className="flex object-cover w-full h-full "
|
||||
className="flex object-cover w-full h-full"
|
||||
imgClassName="object-cover w-full h-full"
|
||||
alt={altTextForPhoto(photo)}
|
||||
priority={priority}
|
||||
|
||||
@ -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
|
||||
>
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
||||
import clsx from 'clsx/lite';
|
||||
import ImageLarge from '@/components/image/ImageLarge';
|
||||
import PhotoRecipe from './PhotoRecipe';
|
||||
import { Photo } from '.';
|
||||
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',
|
||||
)}>
|
||||
<PhotoRecipe {...{
|
||||
recipe,
|
||||
simulation: photo.filmSimulation ?? 'provia',
|
||||
exposure: photo.exposureCompensationFormatted ?? '+0ev',
|
||||
iso: photo.isoFormatted ?? 'ISO 0',
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -6,9 +6,10 @@ import {
|
||||
import {
|
||||
getUniqueCameras,
|
||||
getUniqueFilmSimulations,
|
||||
getUniqueRecipes,
|
||||
getUniqueTags,
|
||||
} from '@/photo/db/query';
|
||||
import { SHOW_FILM_SIMULATIONS } from '@/app/config';
|
||||
import { SHOW_FILM_SIMULATIONS, SHOW_RECIPES } from '@/app/config';
|
||||
import { sortTagsObject } from '@/tag';
|
||||
|
||||
export const getPhotoSidebarData = () => [
|
||||
@ -17,6 +18,9 @@ export const getPhotoSidebarData = () => [
|
||||
SHOW_FILM_SIMULATIONS
|
||||
? getUniqueFilmSimulations().catch(() => [])
|
||||
: [],
|
||||
SHOW_RECIPES
|
||||
? getUniqueRecipes().catch(() => [])
|
||||
: [],
|
||||
] as const;
|
||||
|
||||
export const getPhotoSidebarDataCached = () => [
|
||||
|
||||
@ -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,11 +111,18 @@ const FORM_METADATA = (
|
||||
shouldHide: ({ make }) => make !== MAKE_FUJIFILM,
|
||||
shouldNotOverwriteWithNullDataOnSync: true,
|
||||
},
|
||||
fujifilmRecipe: {
|
||||
type: 'textarea',
|
||||
label: 'fujifilm recipe',
|
||||
recipeTitle: {
|
||||
label: 'recipe title',
|
||||
spellCheck: false,
|
||||
capitalize: false,
|
||||
},
|
||||
recipeData: {
|
||||
type: 'textarea',
|
||||
label: 'recipe data',
|
||||
spellCheck: false,
|
||||
capitalize: false,
|
||||
shouldHide: ({ make }) => make !== MAKE_FUJIFILM,
|
||||
shouldNotOverwriteWithNullDataOnSync: true,
|
||||
validate: value => {
|
||||
let validationMessage = undefined;
|
||||
if (value) {
|
||||
@ -127,8 +134,6 @@ const FORM_METADATA = (
|
||||
}
|
||||
return validationMessage;
|
||||
},
|
||||
shouldHide: ({ make }) => make !== MAKE_FUJIFILM,
|
||||
shouldNotOverwriteWithNullDataOnSync: true,
|
||||
},
|
||||
focalLength: { label: 'focal length' },
|
||||
focalLengthIn35MmFormat: { label: 'focal length 35mm-equivalent' },
|
||||
@ -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 {
|
||||
@ -113,6 +114,7 @@ export interface PhotoSetCategory {
|
||||
camera?: Camera
|
||||
simulation?: FilmSimulation
|
||||
focal?: number
|
||||
recipe?: string
|
||||
lens?: Lens // Unimplemented as a set
|
||||
}
|
||||
|
||||
@ -141,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),
|
||||
@ -164,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) => [
|
||||
|
||||
62
src/recipe/PhotoRecipe.tsx
Normal file
62
src/recipe/PhotoRecipe.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { pathForRecipe } from '@/app/paths';
|
||||
import EntityLink, {
|
||||
EntityLinkExternalProps,
|
||||
} from '@/components/primitives/EntityLink';
|
||||
import { TbChecklist } from 'react-icons/tb';
|
||||
import { formatRecipe } from '.';
|
||||
import clsx from 'clsx/lite';
|
||||
import { RefObject } from 'react';
|
||||
|
||||
export default function PhotoRecipe({
|
||||
recipe,
|
||||
type,
|
||||
badged,
|
||||
contrast,
|
||||
prefetch,
|
||||
countOnHover,
|
||||
className,
|
||||
refButton,
|
||||
isOpen,
|
||||
recipeOnClick,
|
||||
}: {
|
||||
recipe: string
|
||||
refButton?: RefObject<HTMLButtonElement | null>
|
||||
isOpen?: boolean
|
||||
recipeOnClick?: () => void
|
||||
countOnHover?: number
|
||||
} & EntityLinkExternalProps) {
|
||||
return (
|
||||
<div className="flex w-full gap-2">
|
||||
<EntityLink
|
||||
title="Recipe"
|
||||
label={formatRecipe(recipe)}
|
||||
href={pathForRecipe(recipe)}
|
||||
icon={<TbChecklist
|
||||
size={16}
|
||||
className={clsx(
|
||||
badged && 'translate-x-[-1px] translate-y-[0.5px]',
|
||||
)}
|
||||
/>}
|
||||
className={className}
|
||||
type={type}
|
||||
badged={badged}
|
||||
contrast={contrast}
|
||||
prefetch={prefetch}
|
||||
hoverEntity={countOnHover}
|
||||
/>
|
||||
{recipeOnClick &&
|
||||
<button
|
||||
ref={refButton}
|
||||
onClick={recipeOnClick}
|
||||
className={clsx(
|
||||
'self-start',
|
||||
'px-1 py-0.5',
|
||||
'text-[10px] text-main font-medium tracking-wider',
|
||||
'translate-y-[0.5px]',
|
||||
)}
|
||||
>
|
||||
{isOpen ? 'CLOSE' : 'RECIPE'}
|
||||
</button>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,17 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import LoaderButton from '@/components/primitives/LoaderButton';
|
||||
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
||||
import { FilmSimulation } from '@/simulation';
|
||||
import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation';
|
||||
import clsx from 'clsx/lite';
|
||||
import { ReactNode, RefObject } from 'react';
|
||||
import { IoCloseCircle } from 'react-icons/io5';
|
||||
import { motion } from 'framer-motion';
|
||||
import { RecipeProps } from '.';
|
||||
|
||||
const addSign = (value = 0) => value < 0 ? value : `+${value}`;
|
||||
|
||||
export default function PhotoRecipe({
|
||||
export default function PhotoRecipeOverlay({
|
||||
ref,
|
||||
recipe: {
|
||||
dynamicRange,
|
||||
@ -33,12 +32,8 @@ export default function PhotoRecipe({
|
||||
iso,
|
||||
exposure,
|
||||
onClose,
|
||||
}: {
|
||||
}: RecipeProps & {
|
||||
ref?: RefObject<HTMLDivElement | null>
|
||||
recipe: FujifilmRecipe
|
||||
simulation: FilmSimulation
|
||||
iso?: string
|
||||
exposure?: string
|
||||
onClose?: () => void
|
||||
}) {
|
||||
const whiteBalanceTypeFormatted =
|
||||
150
src/recipe/PhotoRecipeOGTile.tsx
Normal file
150
src/recipe/PhotoRecipeOGTile.tsx
Normal file
@ -0,0 +1,150 @@
|
||||
'use client';
|
||||
|
||||
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
||||
import { FilmSimulation } from '@/simulation';
|
||||
import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation';
|
||||
import clsx from 'clsx/lite';
|
||||
import { ReactNode, RefObject } from 'react';
|
||||
|
||||
const addSign = (value = 0) => value < 0 ? value : `+${value}`;
|
||||
|
||||
export default function PhotoRecipeOGTile({
|
||||
recipe: {
|
||||
dynamicRange,
|
||||
whiteBalance,
|
||||
highISONoiseReduction,
|
||||
noiseReductionBasic,
|
||||
highlight,
|
||||
shadow,
|
||||
color,
|
||||
sharpness,
|
||||
clarity,
|
||||
colorChromeEffect,
|
||||
colorChromeFXBlue,
|
||||
grainEffect,
|
||||
bwAdjustment,
|
||||
bwMagentaGreen,
|
||||
},
|
||||
simulation,
|
||||
iso,
|
||||
exposure,
|
||||
}: {
|
||||
ref?: RefObject<HTMLDivElement | null>
|
||||
recipe: FujifilmRecipe
|
||||
simulation: FilmSimulation
|
||||
iso?: string
|
||||
exposure?: string
|
||||
onClose?: () => void
|
||||
}) {
|
||||
const whiteBalanceTypeFormatted =
|
||||
whiteBalance.type === 'kelvin' && whiteBalance.colorTemperature
|
||||
? `${whiteBalance.colorTemperature}K`
|
||||
: whiteBalance.type
|
||||
.replace(/auto./i, '')
|
||||
.replaceAll('-', ' ');
|
||||
|
||||
const renderRow = (children: ReactNode, className?: string) =>
|
||||
<div className={clsx(
|
||||
'flex gap-2 *:w-full *:grow',
|
||||
className,
|
||||
)}>
|
||||
{children}
|
||||
</div>;
|
||||
|
||||
const renderDataSquare = (
|
||||
value: ReactNode,
|
||||
label?: string,
|
||||
className?: string,
|
||||
) => (
|
||||
<div className={clsx(
|
||||
'flex flex-col items-center justify-center gap-0.5 rounded-md min-w-0',
|
||||
'rounded-md border',
|
||||
'border-transparent',
|
||||
'bg-neutral-100/60',
|
||||
label && 'p-1',
|
||||
className,
|
||||
)}>
|
||||
<div className="truncate max-w-full tracking-wide text-lg">
|
||||
{typeof value === 'number' ? addSign(value) : value}
|
||||
</div>
|
||||
{label && <div className={clsx(
|
||||
'text-[11px] leading-none tracking-wide font-medium text-black/50',
|
||||
'uppercase',
|
||||
)}>
|
||||
{label}
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex z-10',
|
||||
'w-[37rem] p-10 aspect-video',
|
||||
'text-[13.5px] text-black',
|
||||
'bg-white/50',
|
||||
'backdrop-blur-xl saturate-[300%]',
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
{renderRow(<>
|
||||
<div className={clsx(
|
||||
'flex',
|
||||
'text-lg leading-none text-black truncate',
|
||||
)}>
|
||||
KODAK PORTRA 500
|
||||
</div>
|
||||
<PhotoFilmSimulation
|
||||
contrast="frosted"
|
||||
simulation={simulation}
|
||||
className="w-auto! grow-0!"
|
||||
/>
|
||||
</>, 'flex items-center gap-4')}
|
||||
{renderRow(<>
|
||||
{renderDataSquare(`DR${dynamicRange.development}`)}
|
||||
{renderDataSquare(iso)}
|
||||
{renderDataSquare(exposure ?? '0ev')}
|
||||
</>)}
|
||||
{renderRow(<>
|
||||
{renderDataSquare(
|
||||
whiteBalanceTypeFormatted.toUpperCase(),
|
||||
`R${addSign(whiteBalance?.red)} / B${addSign(whiteBalance?.blue)}`,
|
||||
)}
|
||||
{renderDataSquare(
|
||||
highISONoiseReduction ?? noiseReductionBasic ?? 'OFF',
|
||||
'ISO NR',
|
||||
)}
|
||||
{renderDataSquare(highlight, 'Highlight')}
|
||||
{renderDataSquare(shadow, 'Shadow')}
|
||||
</>)}
|
||||
{renderRow(<>
|
||||
{renderDataSquare(color, 'Color')}
|
||||
{renderDataSquare(sharpness, 'Sharpness')}
|
||||
{renderDataSquare(clarity, 'Clarity')}
|
||||
</>)}
|
||||
{renderRow(<>
|
||||
{renderDataSquare(
|
||||
colorChromeEffect?.toLocaleUpperCase() ?? 'N/A',
|
||||
'Color Chrome',
|
||||
)}
|
||||
{renderDataSquare(
|
||||
colorChromeFXBlue?.toLocaleUpperCase() ?? 'N/A',
|
||||
'FX Blue',
|
||||
)}
|
||||
</>)}
|
||||
{renderRow(<>
|
||||
{renderDataSquare(
|
||||
grainEffect.roughness.toLocaleUpperCase(),
|
||||
grainEffect.size === 'large'
|
||||
? 'Large Grain'
|
||||
: grainEffect.size === 'small'
|
||||
? 'Small Grain'
|
||||
: 'Grain',
|
||||
)}
|
||||
{renderDataSquare(bwAdjustment ?? 0, 'BW ADJ')}
|
||||
{renderDataSquare(bwMagentaGreen ?? 0, 'BW M/G')}
|
||||
</>)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
55
src/recipe/RecipeHeader.tsx
Normal file
55
src/recipe/RecipeHeader.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import { Photo, PhotoDateRange } from '@/photo';
|
||||
import PhotoHeader from '@/photo/PhotoHeader';
|
||||
import PhotoRecipe from './PhotoRecipe';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import { descriptionForRecipePhotos, photoHasRecipe } from '.';
|
||||
export default function RecipeHeader({
|
||||
recipe,
|
||||
photos,
|
||||
selectedPhoto,
|
||||
indexNumber,
|
||||
count,
|
||||
dateRange,
|
||||
}: {
|
||||
recipe: string
|
||||
photos: Photo[]
|
||||
selectedPhoto?: Photo
|
||||
indexNumber?: number
|
||||
count?: number
|
||||
dateRange?: PhotoDateRange
|
||||
}) {
|
||||
const { setRecipeModalProps } = useAppState();
|
||||
|
||||
const photo = photoHasRecipe(selectedPhoto)
|
||||
? selectedPhoto
|
||||
: photos.find(photoHasRecipe);
|
||||
|
||||
return (
|
||||
<PhotoHeader
|
||||
recipe={recipe}
|
||||
entity={<PhotoRecipe
|
||||
recipe={recipe}
|
||||
contrast="high"
|
||||
recipeOnClick={() => (
|
||||
photo?.recipeData &&
|
||||
photo?.filmSimulation
|
||||
) ? setRecipeModalProps?.({
|
||||
simulation: photo.filmSimulation,
|
||||
recipe: photo.recipeData,
|
||||
iso: photo.isoFormatted,
|
||||
exposure: photo.exposureTimeFormatted,
|
||||
})
|
||||
: undefined}
|
||||
/>}
|
||||
entityDescription={descriptionForRecipePhotos(photos, undefined, count)}
|
||||
photos={photos}
|
||||
selectedPhoto={selectedPhoto}
|
||||
indexNumber={indexNumber}
|
||||
count={count}
|
||||
dateRange={dateRange}
|
||||
includeShareButton
|
||||
/>
|
||||
);
|
||||
}
|
||||
25
src/recipe/RecipeModal.tsx
Normal file
25
src/recipe/RecipeModal.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import Modal from '@/components/Modal';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import PhotoRecipeOverlay from './PhotoRecipeGrid';
|
||||
|
||||
export default function ShareModals() {
|
||||
const {
|
||||
recipeModalProps,
|
||||
setRecipeModalProps,
|
||||
} = useAppState();
|
||||
|
||||
if (recipeModalProps) {
|
||||
return <Modal
|
||||
className="bg-transparent!"
|
||||
onClose={() => setRecipeModalProps?.(undefined)}
|
||||
container={false}
|
||||
>
|
||||
<PhotoRecipeOverlay {...{
|
||||
...recipeModalProps,
|
||||
onClose: () => setRecipeModalProps?.(undefined),
|
||||
}}/>
|
||||
</Modal>;
|
||||
}
|
||||
}
|
||||
42
src/recipe/RecipeOGTile.tsx
Normal file
42
src/recipe/RecipeOGTile.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { Photo, PhotoDateRange } from '@/photo';
|
||||
import { absolutePathForRecipeImage, pathForRecipe } from '@/app/paths';
|
||||
import OGTile from '@/components/OGTile';
|
||||
import { descriptionForRecipePhotos, titleForRecipe } from '.';
|
||||
|
||||
export type OGLoadingState = 'unloaded' | 'loading' | 'loaded' | 'failed';
|
||||
|
||||
export default function RecipeOGTile({
|
||||
recipe,
|
||||
photos,
|
||||
loadingState: loadingStateExternal,
|
||||
riseOnHover,
|
||||
onLoad,
|
||||
onFail,
|
||||
retryTime,
|
||||
count,
|
||||
dateRange,
|
||||
}: {
|
||||
recipe: string
|
||||
photos: Photo[]
|
||||
loadingState?: OGLoadingState
|
||||
onLoad?: () => void
|
||||
onFail?: () => void
|
||||
riseOnHover?: boolean
|
||||
retryTime?: number
|
||||
count?: number
|
||||
dateRange?: PhotoDateRange
|
||||
}) {
|
||||
return (
|
||||
<OGTile {...{
|
||||
title: titleForRecipe(recipe, photos, count),
|
||||
description: descriptionForRecipePhotos(photos, true, count, dateRange),
|
||||
path: pathForRecipe(recipe),
|
||||
pathImageAbsolute: absolutePathForRecipeImage(recipe),
|
||||
loadingState: loadingStateExternal,
|
||||
onLoad,
|
||||
onFail,
|
||||
riseOnHover,
|
||||
retryTime,
|
||||
}}/>
|
||||
);
|
||||
};
|
||||
33
src/recipe/RecipeOverview.tsx
Normal file
33
src/recipe/RecipeOverview.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { Photo, PhotoDateRange } from '@/photo';
|
||||
import PhotoGridContainer from '@/photo/PhotoGridContainer';
|
||||
import RecipeHeader from './RecipeHeader';
|
||||
|
||||
export default function RecipeOverview({
|
||||
recipe,
|
||||
photos,
|
||||
count,
|
||||
dateRange,
|
||||
animateOnFirstLoadOnly,
|
||||
}: {
|
||||
recipe: string,
|
||||
photos: Photo[],
|
||||
count: number,
|
||||
dateRange?: PhotoDateRange,
|
||||
animateOnFirstLoadOnly?: boolean,
|
||||
}) {
|
||||
return (
|
||||
<PhotoGridContainer {...{
|
||||
cacheKey: `recipe-${recipe}`,
|
||||
photos,
|
||||
count,
|
||||
recipe,
|
||||
header: <RecipeHeader {...{
|
||||
recipe,
|
||||
photos,
|
||||
count,
|
||||
dateRange,
|
||||
}} />,
|
||||
animateOnFirstLoadOnly,
|
||||
}} />
|
||||
);
|
||||
}
|
||||
23
src/recipe/RecipeShareModal.tsx
Normal file
23
src/recipe/RecipeShareModal.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { absolutePathForRecipe } from '@/app/paths';
|
||||
import { PhotoSetAttributes } from '../photo';
|
||||
import ShareModal from '@/share/ShareModal';
|
||||
import { shareTextForRecipe } from '.';
|
||||
import RecipeOGTile from './RecipeOGTile';
|
||||
|
||||
export default function RecipeShareModal({
|
||||
recipe,
|
||||
photos,
|
||||
count,
|
||||
dateRange,
|
||||
}: {
|
||||
recipe: string
|
||||
} & PhotoSetAttributes) {
|
||||
return (
|
||||
<ShareModal
|
||||
pathShare={absolutePathForRecipe(recipe)}
|
||||
socialText={shareTextForRecipe(recipe)}
|
||||
>
|
||||
<RecipeOGTile {...{ recipe, photos, count, dateRange }} />
|
||||
</ShareModal>
|
||||
);
|
||||
};
|
||||
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 }),
|
||||
]);
|
||||
71
src/recipe/index.ts
Normal file
71
src/recipe/index.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { absolutePathForRecipe, absolutePathForRecipeImage } from '@/app/paths';
|
||||
import { descriptionForPhotoSet, Photo, photoQuantityText } from '@/photo';
|
||||
import { PhotoDateRange } from '@/photo';
|
||||
import { capitalizeWords } from '@/utility/string';
|
||||
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
||||
import { FilmSimulation } from '@/simulation';
|
||||
|
||||
export type RecipeWithCount = {
|
||||
recipe: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export type Recipes = RecipeWithCount[]
|
||||
|
||||
export interface RecipeProps {
|
||||
recipe: FujifilmRecipe
|
||||
simulation: FilmSimulation
|
||||
iso?: string
|
||||
exposure?: string
|
||||
}
|
||||
|
||||
export const formatRecipe = (recipe?: string) =>
|
||||
capitalizeWords(recipe?.replaceAll('-', ' '));
|
||||
|
||||
export const titleForRecipe = (
|
||||
recipe: string,
|
||||
photos:Photo[] = [],
|
||||
explicitCount?: number,
|
||||
) => [
|
||||
`Recipe: ${formatRecipe(recipe)}`,
|
||||
photoQuantityText(explicitCount ?? photos.length),
|
||||
].join(' ');
|
||||
|
||||
export const shareTextForRecipe = (recipe: string) =>
|
||||
`${formatRecipe(recipe)} recipe photos`;
|
||||
|
||||
export const descriptionForRecipePhotos = (
|
||||
photos: Photo[] = [],
|
||||
dateBased?: boolean,
|
||||
explicitCount?: number,
|
||||
explicitDateRange?: PhotoDateRange,
|
||||
) =>
|
||||
descriptionForPhotoSet(
|
||||
photos,
|
||||
undefined,
|
||||
dateBased,
|
||||
explicitCount,
|
||||
explicitDateRange,
|
||||
);
|
||||
|
||||
export const generateMetaForRecipe = (
|
||||
recipe: string,
|
||||
photos: Photo[],
|
||||
explicitCount?: number,
|
||||
explicitDateRange?: PhotoDateRange,
|
||||
) => ({
|
||||
url: absolutePathForRecipe(recipe),
|
||||
title: titleForRecipe(recipe, photos, explicitCount),
|
||||
description:
|
||||
descriptionForRecipePhotos(photos, true, explicitCount, explicitDateRange),
|
||||
images: absolutePathForRecipeImage(recipe),
|
||||
});
|
||||
|
||||
export const photoHasRecipe = (photo?: Photo) =>
|
||||
photo?.filmSimulation && photo?.recipeData;
|
||||
|
||||
export const sortRecipesWithCount = (
|
||||
a: RecipeWithCount,
|
||||
b: RecipeWithCount,
|
||||
) =>
|
||||
a.recipe.localeCompare(b.recipe);
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -6,6 +6,7 @@ import CameraShareModal from '@/camera/CameraShareModal';
|
||||
import FilmSimulationShareModal from '@/simulation/FilmSimulationShareModal';
|
||||
import FocalLengthShareModal from '@/focal/FocalLengthShareModal';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import RecipeShareModal from '@/recipe/RecipeShareModal';
|
||||
|
||||
export default function ShareModals() {
|
||||
const { shareModalProps = {} } = useAppState();
|
||||
@ -18,6 +19,7 @@ export default function ShareModals() {
|
||||
tag,
|
||||
camera,
|
||||
simulation,
|
||||
recipe,
|
||||
focal,
|
||||
} = shareModalProps;
|
||||
|
||||
@ -33,6 +35,8 @@ export default function ShareModals() {
|
||||
return <FilmSimulationShareModal {...{simulation, ...attributes}} />;
|
||||
} else if (focal !== undefined) {
|
||||
return <FocalLengthShareModal {...{focal, ...attributes}} />;
|
||||
} else if (recipe) {
|
||||
return <RecipeShareModal {...{recipe, ...attributes}} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,11 +15,11 @@ export default function PhotoFilmSimulation({
|
||||
contrast = 'low',
|
||||
prefetch,
|
||||
countOnHover,
|
||||
className,
|
||||
}: {
|
||||
simulation: FilmSimulation
|
||||
countOnHover?: number
|
||||
recipe?: FujifilmRecipe
|
||||
className?: string
|
||||
} & EntityLinkExternalProps) {
|
||||
const { small, medium, large } = labelForFilmSimulation(simulation);
|
||||
|
||||
@ -34,6 +34,7 @@ export default function PhotoFilmSimulation({
|
||||
/>}
|
||||
title={`Film Simulation: ${large}`}
|
||||
type={type}
|
||||
className={className}
|
||||
badged={badged}
|
||||
contrast={contrast}
|
||||
prefetch={prefetch}
|
||||
|
||||
@ -10,6 +10,7 @@ import { ShareModalProps } from '@/share';
|
||||
import { InsightsIndicatorStatus } from '@/admin/insights';
|
||||
import { INITIAL_UPLOAD_STATE, UploadState } from '@/admin/upload';
|
||||
import { AdminData } from '@/admin/actions';
|
||||
import { RecipeProps } from '@/recipe';
|
||||
|
||||
export type AppStateContext = {
|
||||
// CORE
|
||||
@ -34,6 +35,8 @@ export type AppStateContext = {
|
||||
setIsCommandKOpen?: Dispatch<SetStateAction<boolean>>
|
||||
shareModalProps?: ShareModalProps
|
||||
setShareModalProps?: Dispatch<SetStateAction<ShareModalProps | undefined>>
|
||||
recipeModalProps?: RecipeProps
|
||||
setRecipeModalProps?: Dispatch<SetStateAction<RecipeProps | undefined>>
|
||||
// AUTH
|
||||
userEmail?: string
|
||||
setUserEmail?: Dispatch<SetStateAction<string | undefined>>
|
||||
|
||||
@ -23,6 +23,7 @@ import {
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
import { isPathAdmin, PATH_SIGN_IN } from '@/app/paths';
|
||||
import { INITIAL_UPLOAD_STATE, UploadState } from '@/admin/upload';
|
||||
import { RecipeProps } from '@/recipe';
|
||||
|
||||
export default function AppStateProvider({
|
||||
children,
|
||||
@ -53,6 +54,8 @@ export default function AppStateProvider({
|
||||
useState(false);
|
||||
const [shareModalProps, setShareModalProps] =
|
||||
useState<ShareModalProps>();
|
||||
const [recipeModalProps, setRecipeModalProps] =
|
||||
useState<RecipeProps>();
|
||||
// AUTH
|
||||
const [userEmail, setUserEmail] =
|
||||
useState<string>();
|
||||
@ -170,6 +173,8 @@ export default function AppStateProvider({
|
||||
setIsCommandKOpen,
|
||||
shareModalProps,
|
||||
setShareModalProps,
|
||||
recipeModalProps,
|
||||
setRecipeModalProps,
|
||||
// AUTH
|
||||
userEmail,
|
||||
setUserEmail,
|
||||
|
||||
@ -12,6 +12,7 @@ export default function FavsTag({
|
||||
contrast,
|
||||
prefetch,
|
||||
countOnHover,
|
||||
className,
|
||||
}: {
|
||||
countOnHover?: number
|
||||
} & EntityLinkExternalProps) {
|
||||
@ -36,6 +37,7 @@ export default function FavsTag({
|
||||
)}
|
||||
/>}
|
||||
type={type}
|
||||
className={className}
|
||||
hoverEntity={countOnHover}
|
||||
badged={badged}
|
||||
contrast={contrast}
|
||||
|
||||
@ -11,6 +11,7 @@ export default function HiddenTag({
|
||||
contrast,
|
||||
prefetch,
|
||||
countOnHover,
|
||||
className,
|
||||
}: {
|
||||
countOnHover?: number
|
||||
} & EntityLinkExternalProps) {
|
||||
@ -28,6 +29,7 @@ export default function HiddenTag({
|
||||
href={pathForTag(TAG_HIDDEN)}
|
||||
icon={!badged && <AiOutlineEyeInvisible size={16} />}
|
||||
type={type}
|
||||
className={className}
|
||||
hoverEntity={countOnHover}
|
||||
badged={badged}
|
||||
contrast={contrast}
|
||||
|
||||
@ -12,6 +12,7 @@ export default function PhotoTag({
|
||||
contrast,
|
||||
prefetch,
|
||||
countOnHover,
|
||||
className,
|
||||
}: {
|
||||
tag: string
|
||||
countOnHover?: number
|
||||
@ -25,6 +26,7 @@ export default function PhotoTag({
|
||||
className="translate-y-[0.5px]"
|
||||
/>}
|
||||
type={type}
|
||||
className={className}
|
||||
badged={badged}
|
||||
contrast={contrast}
|
||||
prefetch={prefetch}
|
||||
|
||||
@ -51,9 +51,9 @@ export const shareTextForTag = (tag: string) =>
|
||||
|
||||
export const sortTags = (
|
||||
tags: string[],
|
||||
tagToHide?: string,
|
||||
tagToExclude?: string,
|
||||
) => tags
|
||||
.filter(tag => tag !== tagToHide)
|
||||
.filter(tag => tag !== tagToExclude)
|
||||
.sort((a, b) => isTagFavs(a) ? -1 : a.localeCompare(b));
|
||||
|
||||
export const sortTagsObject = (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user