Apply full photo set behavior to all sets

This commit is contained in:
Sam Becker 2024-05-20 00:08:23 -05:00
parent dcfc04c842
commit bc87d2ec0f
16 changed files with 185 additions and 98 deletions

View File

@ -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<Metadata> {
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}
<PhotoDetailPage {...{ photo, photos, simulation, count, dateRange }} />
<PhotoDetailPage {...{
photo,
photos,
photosGrid,
simulation,
indexNumber,
count,
dateRange,
}} />
</>;
}

View File

@ -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<Metadata> {
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}
<PhotoDetailPage
photo={photo}
photos={photos}
photosGrid={photos.slice(
isPhotoFirst ? 1 : 2,
isPhotoFirst
? RELATED_GRID_PHOTOS_TO_SHOW + 1
: RELATED_GRID_PHOTOS_TO_SHOW + 2,
)}
/>
<PhotoDetailPage {...{ photo, photos, photosGrid }} />
</>;
}

View File

@ -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<Metadata> {
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}
<PhotoDetailPage {...{ photo, photos, camera, count, dateRange }} />
<PhotoDetailPage {...{
photo,
photos,
photosGrid,
camera,
indexNumber,
count,
dateRange,
}} />
</>;
}

View File

@ -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<Metadata> {
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}
<PhotoDetailPage {...{ photo, photos, tag, count, dateRange }} />
<PhotoDetailPage {...{
photo,
photos,
photosGrid,
tag,
indexNumber,
count,
dateRange,
}} />
</>;
}

View File

@ -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<Metadata> {
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 (
<PhotoDetailPage {...{ photo, photos, count, tag: TAG_HIDDEN }} />
<PhotoDetailPage {...{
photo,
photos,
photosGrid,
indexNumber,
count,
dateRange,
tag: TAG_HIDDEN,
}} />
);
}

View File

@ -44,7 +44,7 @@ export const {
},
});
export const safelyRunAdminServerAction = async <T>(
export const runAuthenticatedAdminServerAction = async <T>(
callback: () => T,
): Promise<T> => {
const session = await auth();

View File

@ -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}
/>

View File

@ -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({
? <HiddenHeader
photos={photos}
selectedPhoto={photo}
indexNumber={indexNumber}
count={count ?? 0}
/>
: <TagHeader
@ -48,6 +51,8 @@ export default function PhotoDetailPage({
tag={tag}
photos={photos}
selectedPhoto={photo}
indexNumber={indexNumber}
count={count}
dateRange={dateRange}
/>}
/>}
@ -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}
/>}

View File

@ -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 &&
<ShareButton

View File

@ -38,7 +38,7 @@ import {
import { blurImageFromUrl, extractImageDataFromBlobPath } from './server';
import { TAG_FAVS, isTagFavs } from '@/tag';
import { convertPhotoToPhotoDbInsert } from '.';
import { safelyRunAdminServerAction } from '@/auth';
import { runAuthenticatedAdminServerAction } from '@/auth';
import { AI_IMAGE_QUERIES, AiImageQuery } from './ai';
import { streamOpenAiImageQuery } from '@/services/openai';
import { BLUR_ENABLED } from '@/site/config';
@ -46,7 +46,7 @@ import { BLUR_ENABLED } from '@/site/config';
// Private actions
export const createPhotoAction = async (formData: FormData) =>
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<PhotoFormData>,
): Promise<Partial<PhotoFormData>> =>
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);

View File

@ -141,11 +141,23 @@ export const getPhotosNearIdCached = (
...args: Parameters<typeof getPhotosNearId>
) => 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(

View File

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

View File

@ -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 {

View File

@ -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}
/>

View File

@ -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}
/>
);
}

View File

@ -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}
/>