diff --git a/src/app/film/[simulation]/[photoId]/layout.tsx b/src/app/film/[simulation]/[photoId]/layout.tsx index daafa883..cba372b1 100644 --- a/src/app/film/[simulation]/[photoId]/layout.tsx +++ b/src/app/film/[simulation]/[photoId]/layout.tsx @@ -1,4 +1,5 @@ import { + RELATED_GRID_PHOTOS_TO_SHOW, descriptionForPhoto, titleForPhoto, } from '@/photo'; @@ -10,12 +11,21 @@ import { absolutePathForPhotoImage, } from '@/site/paths'; import PhotoDetailPage from '@/photo/PhotoDetailPage'; -import { getPhotoCached } from '@/photo/cache'; import { ReactNode, cache } from 'react'; import { FilmSimulation } from '@/simulation'; -import { getPhotosFilmSimulationDataCached } from '@/simulation/data'; +import { + getPhotosFilmSimulationMetaCached, + getPhotosNearIdCached, +} from '@/photo/cache'; -const getPhotoCachedCached = cache(getPhotoCached); +const getPhotosNearIdCachedCached = cache(( + photoId: string, + simulation: FilmSimulation, +) => + getPhotosNearIdCached( + photoId, + { simulation, limit: RELATED_GRID_PHOTOS_TO_SHOW + 2 }, + )); interface PhotoFilmSimulationProps { params: { photoId: string, simulation: FilmSimulation } @@ -24,7 +34,7 @@ interface PhotoFilmSimulationProps { export async function generateMetadata({ params: { photoId, simulation }, }: PhotoFilmSimulationProps): Promise { - const photo = await getPhotoCachedCached(photoId); + const { photo } = await getPhotosNearIdCachedCached(photoId, simulation); if (!photo) { return {}; } @@ -55,17 +65,24 @@ export default async function PhotoFilmSimulationPage({ params: { photoId, simulation }, children, }: PhotoFilmSimulationProps & { children: ReactNode }) { - const photo = await getPhotoCachedCached(photoId); + const { photo, photos, photosGrid, indexNumber } = + await getPhotosNearIdCachedCached(photoId, simulation); if (!photo) { redirect(PATH_ROOT); } - const [ - photos, - { count, dateRange }, - ] = await getPhotosFilmSimulationDataCached({ simulation }); + const { count, dateRange } = + await getPhotosFilmSimulationMetaCached(simulation); return <> {children} - + ; } diff --git a/src/app/p/[photoId]/layout.tsx b/src/app/p/[photoId]/layout.tsx index fcf5a252..26415c8a 100644 --- a/src/app/p/[photoId]/layout.tsx +++ b/src/app/p/[photoId]/layout.tsx @@ -14,10 +14,10 @@ import PhotoDetailPage from '@/photo/PhotoDetailPage'; import { getPhotosNearIdCached } from '@/photo/cache'; import { IS_PRODUCTION, STATICALLY_OPTIMIZED_PAGES } from '@/site/config'; import { GENERATE_STATIC_PARAMS_LIMIT, getPhotoIds } from '@/photo/db'; -import { cache } from 'react'; +import { ReactNode, cache } from 'react'; -const getPhotosNearIdCachedCached = cache((photoId: string, limit: number) => - getPhotosNearIdCached(photoId, { limit })); +const getPhotosNearIdCachedCached = cache((photoId: string) => + getPhotosNearIdCached(photoId, { limit: RELATED_GRID_PHOTOS_TO_SHOW + 2 })); export let generateStaticParams: (() => Promise<{ photoId: string }[]>) | undefined = undefined; @@ -36,10 +36,7 @@ interface PhotoProps { export async function generateMetadata({ params: { photoId }, }:PhotoProps): Promise { - const { photo } = await getPhotosNearIdCachedCached( - photoId, - RELATED_GRID_PHOTOS_TO_SHOW + 2, - ); + const { photo } = await getPhotosNearIdCachedCached(photoId); if (!photo) { return {}; } @@ -69,27 +66,14 @@ export async function generateMetadata({ export default async function PhotoPage({ params: { photoId }, children, -}: PhotoProps & { children: React.ReactNode }) { - const { photos, photo } = await getPhotosNearIdCachedCached( - photoId, - RELATED_GRID_PHOTOS_TO_SHOW + 2, - ); +}: PhotoProps & { children: ReactNode }) { + const { photo, photos, photosGrid } = + await getPhotosNearIdCachedCached(photoId); if (!photo) { redirect(PATH_ROOT); } - - const isPhotoFirst = photos.findIndex(p => p.id === photoId) === 0; return <> {children} - + ; } diff --git a/src/app/shot-on/[make]/[model]/[photoId]/layout.tsx b/src/app/shot-on/[make]/[model]/[photoId]/layout.tsx index ce0cc352..d1a5c04c 100644 --- a/src/app/shot-on/[make]/[model]/[photoId]/layout.tsx +++ b/src/app/shot-on/[make]/[model]/[photoId]/layout.tsx @@ -1,5 +1,5 @@ import { - INFINITE_SCROLL_GRID_PHOTO_INITIAL, + RELATED_GRID_PHOTOS_TO_SHOW, descriptionForPhoto, titleForPhoto, } from '@/photo'; @@ -11,17 +11,33 @@ import { absolutePathForPhotoImage, } from '@/site/paths'; import PhotoDetailPage from '@/photo/PhotoDetailPage'; -import { getPhotoCached } from '@/photo/cache'; -import { PhotoCameraProps, cameraFromPhoto } from '@/camera'; -import { getPhotosCameraDataCached } from '@/camera/data'; +import { + getPhotosCameraMetaCached, + getPhotosNearIdCached, +} from '@/photo/cache'; +import { + PhotoCameraProps, + cameraFromPhoto, + getCameraFromParams, +} from '@/camera'; import { ReactNode, cache } from 'react'; -const getPhotoCachedCached = cache(getPhotoCached); +const getPhotosNearIdCachedCached = cache(( + photoId: string, + make: string, + model: string, +) => + getPhotosNearIdCached( + photoId, { + camera: getCameraFromParams({ make, model }), + limit: RELATED_GRID_PHOTOS_TO_SHOW + 2, + }, + )); export async function generateMetadata({ params: { photoId, make, model }, }: PhotoCameraProps): Promise { - const photo = await getPhotoCachedCached(photoId); + const { photo } = await getPhotosNearIdCachedCached(photoId, make, model); if (!photo) { return {}; } @@ -56,22 +72,25 @@ export default async function PhotoCameraPage({ params: { photoId, make, model }, children, }: PhotoCameraProps & { children: ReactNode }) { - const photo = await getPhotoCachedCached(photoId); + const { photo, photos, photosGrid, indexNumber } = + await getPhotosNearIdCachedCached(photoId, make, model); if (!photo) { redirect(PATH_ROOT); } - const [ - photos, - { count, dateRange }, - camera, - ] = await getPhotosCameraDataCached( - make, - model, - INFINITE_SCROLL_GRID_PHOTO_INITIAL, - ); + const camera = cameraFromPhoto(photo, { make, model }); + + const { count, dateRange } = await getPhotosCameraMetaCached(camera); return <> {children} - + ; } diff --git a/src/app/tag/[tag]/[photoId]/layout.tsx b/src/app/tag/[tag]/[photoId]/layout.tsx index 93709098..03c25c75 100644 --- a/src/app/tag/[tag]/[photoId]/layout.tsx +++ b/src/app/tag/[tag]/[photoId]/layout.tsx @@ -1,4 +1,5 @@ import { + RELATED_GRID_PHOTOS_TO_SHOW, descriptionForPhoto, titleForPhoto, } from '@/photo'; @@ -10,11 +11,17 @@ import { absolutePathForPhotoImage, } from '@/site/paths'; import PhotoDetailPage from '@/photo/PhotoDetailPage'; -import { getPhotoCached } from '@/photo/cache'; -import { getPhotosTagDataCached } from '@/tag/data'; +import { + getPhotosNearIdCached, + getPhotosTagMetaCached, +} from '@/photo/cache'; import { ReactNode, cache } from 'react'; -const getPhotoCachedCached = cache(getPhotoCached); +const getPhotosNearIdCachedCached = cache((photoId: string, tag: string) => + getPhotosNearIdCached( + photoId, + { tag, limit: RELATED_GRID_PHOTOS_TO_SHOW + 2 }, + )); interface PhotoTagProps { params: { photoId: string, tag: string } @@ -23,7 +30,7 @@ interface PhotoTagProps { export async function generateMetadata({ params: { photoId, tag }, }: PhotoTagProps): Promise { - const photo = await getPhotoCachedCached(photoId); + const { photo } = await getPhotosNearIdCachedCached(photoId, tag); if (!photo) { return {}; } @@ -54,17 +61,23 @@ export default async function PhotoTagPage({ params: { photoId, tag }, children, }: PhotoTagProps & { children: ReactNode }) { - const photo = await getPhotoCachedCached(photoId); + const { photo, photos, photosGrid, indexNumber } = + await getPhotosNearIdCachedCached(photoId, tag); if (!photo) { redirect(PATH_ROOT); } - const [ - photos, - { count, dateRange }, - ] = await getPhotosTagDataCached({ tag }); + const { count, dateRange } = await getPhotosTagMetaCached(tag); return <> {children} - + ; } diff --git a/src/app/tag/hidden/[photoId]/page.tsx b/src/app/tag/hidden/[photoId]/page.tsx index 5f7e4e91..2382bd3b 100644 --- a/src/app/tag/hidden/[photoId]/page.tsx +++ b/src/app/tag/hidden/[photoId]/page.tsx @@ -1,13 +1,24 @@ -import { descriptionForPhoto, titleForPhoto } from '@/photo'; +import { + RELATED_GRID_PHOTOS_TO_SHOW, + descriptionForPhoto, + titleForPhoto, +} from '@/photo'; import PhotoDetailPage from '@/photo/PhotoDetailPage'; -import { getPhotoCached, getPhotosCached } from '@/photo/cache'; +import { + getPhotosNearIdCached, + getPhotosTagHiddenMetaCached, +} from '@/photo/cache'; import { PATH_ROOT, absolutePathForPhoto } from '@/site/paths'; import { TAG_HIDDEN } from '@/tag'; import { Metadata } from 'next'; import { redirect } from 'next/navigation'; import { cache } from 'react'; -const getPhotoCachedCached = cache(getPhotoCached); +const getPhotosNearIdCachedCached = cache((photoId: string) => + getPhotosNearIdCached( + photoId, + { hidden: 'only' , limit: RELATED_GRID_PHOTOS_TO_SHOW + 2 }, + )); interface PhotoTagProps { params: { photoId: string } @@ -16,7 +27,7 @@ interface PhotoTagProps { export async function generateMetadata({ params: { photoId }, }: PhotoTagProps): Promise { - const photo = await getPhotoCachedCached(photoId, true); + const { photo } = await getPhotosNearIdCachedCached(photoId); if (!photo) { return {}; } @@ -43,14 +54,22 @@ export async function generateMetadata({ export default async function PhotoTagHiddenPage({ params: { photoId }, }: PhotoTagProps) { - const photo = await getPhotoCachedCached(photoId, true); + const { photo, photos, photosGrid, indexNumber } = + await getPhotosNearIdCachedCached(photoId); if (!photo) { redirect(PATH_ROOT); } - const photos = await getPhotosCached({ hidden: 'only' }); - const count = photos.length; + const { count, dateRange } = await getPhotosTagHiddenMetaCached(); return ( - + ); } diff --git a/src/auth/index.ts b/src/auth/index.ts index 20ae70af..da7e9c6e 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -44,7 +44,7 @@ export const { }, }); -export const safelyRunAdminServerAction = async ( +export const runAuthenticatedAdminServerAction = async ( callback: () => T, ): Promise => { const session = await auth(); diff --git a/src/camera/CameraHeader.tsx b/src/camera/CameraHeader.tsx index 35f7b9ef..82675100 100644 --- a/src/camera/CameraHeader.tsx +++ b/src/camera/CameraHeader.tsx @@ -9,12 +9,14 @@ export default function CameraHeader({ camera: cameraProp, photos, selectedPhoto, + indexNumber, count, dateRange, }: { camera: Camera photos: Photo[] selectedPhoto?: Photo + indexNumber?: number count?: number dateRange?: PhotoDateRange }) { @@ -28,6 +30,7 @@ export default function CameraHeader({ photos={photos} selectedPhoto={selectedPhoto} sharePath={pathForCameraShare(camera)} + indexNumber={indexNumber} count={count} dateRange={dateRange} /> diff --git a/src/photo/PhotoDetailPage.tsx b/src/photo/PhotoDetailPage.tsx index f6364342..6f95b22a 100644 --- a/src/photo/PhotoDetailPage.tsx +++ b/src/photo/PhotoDetailPage.tsx @@ -20,6 +20,7 @@ export default function PhotoDetailPage({ tag, camera, simulation, + indexNumber, count, dateRange, }: { @@ -29,6 +30,7 @@ export default function PhotoDetailPage({ tag?: string camera?: Camera simulation?: FilmSimulation + indexNumber?: number count?: number dateRange?: PhotoDateRange }) { @@ -41,6 +43,7 @@ export default function PhotoDetailPage({ ? : } />} @@ -59,6 +64,7 @@ export default function PhotoDetailPage({ camera={camera} photos={photos} selectedPhoto={photo} + indexNumber={indexNumber} count={count} dateRange={dateRange} />} @@ -71,6 +77,7 @@ export default function PhotoDetailPage({ simulation={simulation} photos={photos} selectedPhoto={photo} + indexNumber={indexNumber} count={count} dateRange={dateRange} />} diff --git a/src/photo/PhotoSetHeader.tsx b/src/photo/PhotoSetHeader.tsx index eefb0645..87b4da2f 100644 --- a/src/photo/PhotoSetHeader.tsx +++ b/src/photo/PhotoSetHeader.tsx @@ -13,6 +13,7 @@ export default function PhotoSetHeader({ photos, selectedPhoto, sharePath, + indexNumber, count, dateRange, }: { @@ -22,6 +23,7 @@ export default function PhotoSetHeader({ photos: Photo[] selectedPhoto?: Photo sharePath?: string + indexNumber?: number count?: number dateRange?: PhotoDateRange }) { @@ -59,7 +61,7 @@ export default function PhotoSetHeader({ )}> {selectedPhotoIndex !== undefined // eslint-disable-next-line max-len - ? `${entityVerb ? `${entityVerb} ` : ''}${selectedPhotoIndex + 1} of ${count ?? photos.length}` + ? `${entityVerb ? `${entityVerb} ` : ''}${indexNumber || (selectedPhotoIndex + 1)} of ${count ?? photos.length}` : entityDescription} {selectedPhotoIndex === undefined && sharePath && - safelyRunAdminServerAction(async () => { + runAuthenticatedAdminServerAction(async () => { const photo = convertFormDataToPhotoDbInsert(formData, true); const updatedUrl = await convertUploadToPhoto(photo.url); @@ -60,7 +60,7 @@ export const createPhotoAction = async (formData: FormData) => }); export const updatePhotoAction = async (formData: FormData) => - safelyRunAdminServerAction(async () => { + runAuthenticatedAdminServerAction(async () => { const photo = convertFormDataToPhotoDbInsert(formData); let url: string | undefined; @@ -82,7 +82,7 @@ export const toggleFavoritePhotoAction = async ( photoId: string, shouldRedirect?: boolean, ) => - safelyRunAdminServerAction(async () => { + runAuthenticatedAdminServerAction(async () => { const photo = await getPhoto(photoId); if (photo) { const { tags } = photo; @@ -102,7 +102,7 @@ export const deletePhotoAction = async ( photoUrl: string, shouldRedirect?: boolean, ) => - safelyRunAdminServerAction(async () => { + runAuthenticatedAdminServerAction(async () => { await sqlDeletePhoto(photoId).then(() => deleteStorageUrl(photoUrl)); revalidateAllKeysAndPaths(); if (shouldRedirect) { @@ -111,7 +111,7 @@ export const deletePhotoAction = async ( }); export const deletePhotoFormAction = async (formData: FormData) => - safelyRunAdminServerAction(() => + runAuthenticatedAdminServerAction(() => deletePhotoAction( formData.get('id') as string, formData.get('url') as string, @@ -119,7 +119,7 @@ export const deletePhotoFormAction = async (formData: FormData) => ); export const deletePhotoTagGloballyAction = async (formData: FormData) => - safelyRunAdminServerAction(async () => { + runAuthenticatedAdminServerAction(async () => { const tag = formData.get('tag') as string; await sqlDeletePhotoTagGlobally(tag); @@ -129,7 +129,7 @@ export const deletePhotoTagGloballyAction = async (formData: FormData) => }); export const renamePhotoTagGloballyAction = async (formData: FormData) => - safelyRunAdminServerAction(async () => { + runAuthenticatedAdminServerAction(async () => { const tag = formData.get('tag') as string; const updatedTag = formData.get('updatedTag') as string; @@ -142,7 +142,7 @@ export const renamePhotoTagGloballyAction = async (formData: FormData) => }); export const deleteBlobPhotoAction = async (formData: FormData) => - safelyRunAdminServerAction(async () => { + runAuthenticatedAdminServerAction(async () => { await deleteStorageUrl(formData.get('url') as string); revalidateAdminPaths(); @@ -157,7 +157,7 @@ export const deleteBlobPhotoAction = async (formData: FormData) => export const getExifDataAction = async ( photoFormPrevious: Partial, ): Promise> => - safelyRunAdminServerAction(async () => { + runAuthenticatedAdminServerAction(async () => { const { url } = photoFormPrevious; if (url) { const { photoFormExif } = await extractImageDataFromBlobPath(url); @@ -171,7 +171,7 @@ export const getExifDataAction = async ( // Accessed from admin photo table // will update blur data export const syncPhotoExifDataAction = async (formData: FormData) => - safelyRunAdminServerAction(async () => { + runAuthenticatedAdminServerAction(async () => { const photoId = formData.get('id') as string; if (photoId) { const photo = await getPhoto(photoId); @@ -193,32 +193,32 @@ export const syncPhotoExifDataAction = async (formData: FormData) => }); export const syncCacheAction = async () => - safelyRunAdminServerAction(revalidateAllKeysAndPaths); + runAuthenticatedAdminServerAction(revalidateAllKeysAndPaths); export const streamAiImageQueryAction = async ( imageBase64: string, query: AiImageQuery, ) => - safelyRunAdminServerAction(() => + runAuthenticatedAdminServerAction(() => streamOpenAiImageQuery(imageBase64, AI_IMAGE_QUERIES[query])); export const getImageBlurAction = async (url: string) => - safelyRunAdminServerAction(() => blurImageFromUrl(url)); + runAuthenticatedAdminServerAction(() => blurImageFromUrl(url)); export const getPhotosTagHiddenMetaCachedAction = async () => - safelyRunAdminServerAction(getPhotosTagHiddenMetaCached); + runAuthenticatedAdminServerAction(getPhotosTagHiddenMetaCached); // Public/Private actions export const getPhotosAction = async (options: GetPhotosOptions) => (options.hidden === 'include' || options.hidden === 'only') - ? safelyRunAdminServerAction(() => + ? runAuthenticatedAdminServerAction(() => getPhotos(options)) : getPhotos(options); export const getPhotosCachedAction = async (options: GetPhotosOptions) => (options.hidden === 'include' || options.hidden === 'only') - ? safelyRunAdminServerAction(() => + ? runAuthenticatedAdminServerAction(() => getPhotosCached (options)) : getPhotosCached(options); diff --git a/src/photo/cache.ts b/src/photo/cache.ts index 20ec6552..6ef73b99 100644 --- a/src/photo/cache.ts +++ b/src/photo/cache.ts @@ -141,11 +141,23 @@ export const getPhotosNearIdCached = ( ...args: Parameters ) => unstable_cache( getPhotosNearId, - [KEY_PHOTOS], -)(...args).then(({ photos, photo }) => ({ - photos: parseCachedPhotosDates(photos), - photo: photo ? parseCachedPhotoDates(photo) : undefined, -})); + [KEY_PHOTOS, ...getPhotosCacheKeys(args[1])], +)(...args).then(({ photos, indexNumber }) => { + const [photoId, { limit }] = args; + const photo = photos.find(({ id }) => id === photoId); + const isPhotoFirst = photos.findIndex(p => p.id === photoId) === 0; + return { + photo: photo ? parseCachedPhotoDates(photo) : undefined, + photos: parseCachedPhotosDates(photos), + ...limit && { + photosGrid: photos.slice( + isPhotoFirst ? 1 : 2, + isPhotoFirst ? limit - 1 : limit, + ), + }, + indexNumber, + }; +}); export const getPhotosDateRangeCached = unstable_cache( diff --git a/src/photo/db.ts b/src/photo/db.ts index e664ddf2..126c1223 100644 --- a/src/photo/db.ts +++ b/src/photo/db.ts @@ -488,10 +488,11 @@ export const getPhotosNearId = async ( ); }, `getPhotosNearId: ${photoId}`) .then(({ rows }) => { - const photos = rows.map(parsePhotoFromDb); + const photo = rows.find(({ id }) => id === photoId); + const indexNumber = photo ? parseInt(photo.row_number) : undefined; return { - photos, - photo: photos.find(photo => photo.id === photoId), + photos: rows.map(parsePhotoFromDb), + indexNumber, }; }); diff --git a/src/services/openai.ts b/src/services/openai.ts index 16a12f47..4c83c570 100644 --- a/src/services/openai.ts +++ b/src/services/openai.ts @@ -6,7 +6,7 @@ import { createOpenAI } from '@ai-sdk/openai'; import { kv } from '@vercel/kv'; import { Ratelimit } from '@upstash/ratelimit'; import { AI_TEXT_GENERATION_ENABLED, HAS_VERCEL_KV } from '@/site/config'; -import { safelyRunAdminServerAction } from '@/auth'; +import { runAuthenticatedAdminServerAction } from '@/auth'; import { removeBase64Prefix } from '@/utility/image'; const RATE_LIMIT_IDENTIFIER = 'openai-image-query'; @@ -28,7 +28,7 @@ export const streamOpenAiImageQuery = async ( imageBase64: string, query: string, ) => { - return safelyRunAdminServerAction(async () => { + return runAuthenticatedAdminServerAction(async () => { if (ratelimit) { let success = false; try { diff --git a/src/simulation/FilmSimulationHeader.tsx b/src/simulation/FilmSimulationHeader.tsx index 3f663a0c..b0908572 100644 --- a/src/simulation/FilmSimulationHeader.tsx +++ b/src/simulation/FilmSimulationHeader.tsx @@ -9,12 +9,14 @@ export default function FilmSimulationHeader({ simulation, photos, selectedPhoto, + indexNumber, count, dateRange, }: { simulation: FilmSimulation photos: Photo[] selectedPhoto?: Photo + indexNumber?: number count?: number dateRange?: PhotoDateRange }) { @@ -27,6 +29,7 @@ export default function FilmSimulationHeader({ photos={photos} selectedPhoto={selectedPhoto} sharePath={pathForFilmSimulationShare(simulation)} + indexNumber={indexNumber} count={count} dateRange={dateRange} /> diff --git a/src/tag/HiddenHeader.tsx b/src/tag/HiddenHeader.tsx index 0d469a0d..feac25fb 100644 --- a/src/tag/HiddenHeader.tsx +++ b/src/tag/HiddenHeader.tsx @@ -5,10 +5,12 @@ import HiddenTag from './HiddenTag'; export default function HiddenHeader({ photos, selectedPhoto, + indexNumber, count, }: { photos: Photo[] selectedPhoto?: Photo + indexNumber?: number count: number }) { return ( @@ -18,6 +20,8 @@ export default function HiddenHeader({ entityDescription={photoQuantityText(count, false)} photos={photos} selectedPhoto={selectedPhoto} + indexNumber={indexNumber} + count={count} /> ); } diff --git a/src/tag/TagHeader.tsx b/src/tag/TagHeader.tsx index eeb812c0..4a9e3165 100644 --- a/src/tag/TagHeader.tsx +++ b/src/tag/TagHeader.tsx @@ -9,12 +9,14 @@ export default function TagHeader({ tag, photos, selectedPhoto, + indexNumber, count, dateRange, }: { tag: string photos: Photo[] selectedPhoto?: Photo + indexNumber?: number count?: number dateRange?: PhotoDateRange }) { @@ -28,6 +30,7 @@ export default function TagHeader({ photos={photos} selectedPhoto={selectedPhoto} sharePath={pathForTagShare(tag)} + indexNumber={indexNumber} count={count} dateRange={dateRange} />