From 5684d423c13853eb1d9abf1794b24f8fa8c6bfef Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 21 May 2024 00:22:06 -0500 Subject: [PATCH 1/6] Add focal length paths --- __tests__/path.test.ts | 67 ++++++++++++++++++----- src/focal/index.ts | 50 +++++++++++++++++ src/simulation/index.ts | 2 +- src/site/Footer.tsx | 4 +- src/site/paths.ts | 115 +++++++++++++++++++++++++--------------- 5 files changed, 181 insertions(+), 57 deletions(-) create mode 100644 src/focal/index.ts diff --git a/__tests__/path.test.ts b/__tests__/path.test.ts index 0552a40f..01cfa03f 100644 --- a/__tests__/path.test.ts +++ b/__tests__/path.test.ts @@ -10,6 +10,10 @@ import { isPathFilmSimulationPhoto, isPathFilmSimulationPhotoShare, isPathFilmSimulationShare, + isPathFocalLength, + isPathFocalLengthPhoto, + isPathFocalLengthPhotoShare, + isPathFocalLengthShare, isPathPhoto, isPathPhotoShare, isPathProtected, @@ -20,13 +24,15 @@ import { } from '@/site/paths'; import { TAG_HIDDEN } from '@/tag'; -const PHOTO_ID = 'UsKSGcbt'; -const TAG = 'tag-name'; -const CAMERA_MAKE = 'fujifilm'; -const CAMERA_MODEL = 'x-t1'; -const CAMERA_OBJECT = { make: CAMERA_MAKE, model: CAMERA_MODEL }; -const FILM_SIMULATION = 'acros'; -const SHARE = 'share'; +const PHOTO_ID = 'UsKSGcbt'; +const TAG = 'tag-name'; +const CAMERA_MAKE = 'fujifilm'; +const CAMERA_MODEL = 'x-t1'; +const CAMERA_OBJECT = { make: CAMERA_MAKE, model: CAMERA_MODEL }; +const FILM_SIMULATION = 'acros'; +const FOCAL_LENGTH_NUMBER = 90; +const FOCAL_LENGTH = `${FOCAL_LENGTH_NUMBER}mm`; +const SHARE = 'share'; const PATH_ROOT = '/'; const PATH_GRID = '/grid'; @@ -54,6 +60,11 @@ const PATH_FILM_SIMULATION = `/film/${FILM_SIMULATION}`; const PATH_FILM_SIMULATION_SHARE = `${PATH_FILM_SIMULATION}/${SHARE}`; const PATH_FILM_SIMULATION_PHOTO = `${PATH_FILM_SIMULATION}/${PHOTO_ID}`; const PATH_FILM_SIMULATION_PHOTO_SHARE = `${PATH_FILM_SIMULATION_PHOTO}/${SHARE}`; + +const PATH_FOCAL_LENGTH = `/focal/${FOCAL_LENGTH}`; +const PATH_FOCAL_LENGTH_SHARE = `${PATH_FOCAL_LENGTH}/${SHARE}`; +const PATH_FOCAL_LENGTH_PHOTO = `${PATH_FOCAL_LENGTH}/${PHOTO_ID}`; +const PATH_FOCAL_LENGTH_PHOTO_SHARE = `${PATH_FOCAL_LENGTH_PHOTO}/${SHARE}`; describe('Paths', () => { it('can be protected', () => { @@ -87,6 +98,10 @@ describe('Paths', () => { expect(isPathFilmSimulationShare(PATH_FILM_SIMULATION_SHARE)).toBe(true); expect(isPathFilmSimulationPhoto(PATH_FILM_SIMULATION_PHOTO)).toBe(true); expect(isPathFilmSimulationPhotoShare(PATH_FILM_SIMULATION_PHOTO_SHARE)).toBe(true); + expect(isPathFocalLength(PATH_FOCAL_LENGTH)).toBe(true); + expect(isPathFocalLengthShare(PATH_FOCAL_LENGTH_SHARE)).toBe(true); + expect(isPathFocalLengthPhoto(PATH_FOCAL_LENGTH_PHOTO)).toBe(true); + expect(isPathFocalLengthPhotoShare(PATH_FOCAL_LENGTH_PHOTO_SHARE)).toBe(true); // Negative expect(isPathPhoto(PATH_TAG_PHOTO_SHARE)).toBe(false); expect(isPathPhotoShare(PATH_TAG_PHOTO)).toBe(false); @@ -102,8 +117,13 @@ describe('Paths', () => { expect(isPathFilmSimulationShare(PATH_TAG)).toBe(false); expect(isPathFilmSimulationPhoto(PATH_PHOTO_SHARE)).toBe(false); expect(isPathFilmSimulationPhotoShare(PATH_PHOTO)).toBe(false); + expect(isPathFocalLength(PATH_FILM_SIMULATION)).toBe(false); + expect(isPathFocalLengthShare(PATH_FILM_SIMULATION_SHARE)).toBe(false); + expect(isPathFocalLengthPhoto(PATH_FILM_SIMULATION_PHOTO)).toBe(false); + expect(isPathFocalLengthPhotoShare(PATH_FILM_SIMULATION_PHOTO_SHARE)).toBe(false); }); it('can be parsed', () => { + // Core expect(getPathComponents(PATH_ROOT)).toEqual({}); expect(getPathComponents(PATH_PHOTO)).toEqual({ photoId: PHOTO_ID, @@ -111,6 +131,7 @@ describe('Paths', () => { expect(getPathComponents(PATH_PHOTO_SHARE)).toEqual({ photoId: PHOTO_ID, }); + // Tag expect(getPathComponents(PATH_TAG)).toEqual({ tag: TAG, }); @@ -125,6 +146,7 @@ describe('Paths', () => { photoId: PHOTO_ID, tag: TAG, }); + // Camera expect(getPathComponents(PATH_CAMERA)).toEqual({ camera: CAMERA_OBJECT, }); @@ -139,6 +161,7 @@ describe('Paths', () => { photoId: PHOTO_ID, camera: CAMERA_OBJECT, }); + // Film Simulation expect(getPathComponents(PATH_FILM_SIMULATION)).toEqual({ simulation: FILM_SIMULATION, }); @@ -153,29 +176,49 @@ describe('Paths', () => { photoId: PHOTO_ID, simulation: FILM_SIMULATION, }); + // Focal Length + expect(getPathComponents(PATH_FOCAL_LENGTH)).toEqual({ + focal: FOCAL_LENGTH_NUMBER, + }); + expect(getPathComponents(PATH_FOCAL_LENGTH_SHARE)).toEqual({ + focal: FOCAL_LENGTH_NUMBER, + }); + expect(getPathComponents(PATH_FOCAL_LENGTH_PHOTO)).toEqual({ + photoId: PHOTO_ID, + focal: FOCAL_LENGTH_NUMBER, + }); + expect(getPathComponents(PATH_FOCAL_LENGTH_PHOTO_SHARE)).toEqual({ + photoId: PHOTO_ID, + focal: FOCAL_LENGTH_NUMBER, + }); }); it('can be escaped', () => { - // Root views + // Root expect(getEscapePath(PATH_ROOT)).toEqual(undefined); expect(getEscapePath(PATH_GRID)).toEqual(undefined); expect(getEscapePath(PATH_ADMIN)).toEqual(undefined); - // Photo views + // Photo expect(getEscapePath(PATH_PHOTO)).toEqual(PATH_GRID); expect(getEscapePath(PATH_PHOTO_SHARE)).toEqual(PATH_PHOTO); - // Tag views + // Tag expect(getEscapePath(PATH_TAG)).toEqual(PATH_GRID); expect(getEscapePath(PATH_TAG_SHARE)).toEqual(PATH_TAG); expect(getEscapePath(PATH_TAG_PHOTO)).toEqual(PATH_TAG); expect(getEscapePath(PATH_TAG_PHOTO_SHARE)).toEqual(PATH_TAG_PHOTO); - // Camera views + // Camera expect(getEscapePath(PATH_CAMERA)).toEqual(PATH_GRID); expect(getEscapePath(PATH_CAMERA_SHARE)).toEqual(PATH_CAMERA); expect(getEscapePath(PATH_CAMERA_PHOTO)).toEqual(PATH_CAMERA); expect(getEscapePath(PATH_CAMERA_PHOTO_SHARE)).toEqual(PATH_CAMERA_PHOTO); - // Film Simulation views + // Film Simulation expect(getEscapePath(PATH_FILM_SIMULATION)).toEqual(PATH_GRID); expect(getEscapePath(PATH_FILM_SIMULATION_SHARE)).toEqual(PATH_FILM_SIMULATION); expect(getEscapePath(PATH_FILM_SIMULATION_PHOTO)).toEqual(PATH_FILM_SIMULATION); expect(getEscapePath(PATH_FILM_SIMULATION_PHOTO_SHARE)).toEqual(PATH_FILM_SIMULATION_PHOTO); + // Focal Length + expect(getEscapePath(PATH_FOCAL_LENGTH)).toEqual(PATH_GRID); + expect(getEscapePath(PATH_FOCAL_LENGTH_SHARE)).toEqual(PATH_FOCAL_LENGTH); + expect(getEscapePath(PATH_FOCAL_LENGTH_PHOTO)).toEqual(PATH_FOCAL_LENGTH); + expect(getEscapePath(PATH_FOCAL_LENGTH_PHOTO_SHARE)).toEqual(PATH_FOCAL_LENGTH_PHOTO); }); }); diff --git a/src/focal/index.ts b/src/focal/index.ts new file mode 100644 index 00000000..712e8a25 --- /dev/null +++ b/src/focal/index.ts @@ -0,0 +1,50 @@ +import { + Photo, + PhotoDateRange, + descriptionForPhotoSet, + photoQuantityText, +} from '@/photo'; +import { + absolutePathForFocalLength, + absolutePathForFocalLengthImage, +} from '@/site/paths'; + +export const titleForFocalLength = ( + focal: number, + photos: Photo[], + explicitCount?: number, +) => [ + `${focal}mm`, + photoQuantityText(explicitCount ?? photos.length), +].join(' '); + +export const descriptionForFocalLengthPhotos = ( + photos: Photo[], + dateBased?: boolean, + explicitCount?: number, + explicitDateRange?: PhotoDateRange, +) => + descriptionForPhotoSet( + photos, + undefined, + dateBased, + explicitCount, + explicitDateRange, + ); + +export const generateMetaForFocalLength = ( + focal: number, + photos: Photo[], + explicitCount?: number, + explicitDateRange?: PhotoDateRange, +) => ({ + url: absolutePathForFocalLength(focal), + title: titleForFocalLength(focal, photos, explicitCount), + description: descriptionForFocalLengthPhotos( + photos, + true, + explicitCount, + explicitDateRange, + ), + images: absolutePathForFocalLengthImage(focal), +}); diff --git a/src/simulation/index.ts b/src/simulation/index.ts index 5a7b82fc..bb571b73 100644 --- a/src/simulation/index.ts +++ b/src/simulation/index.ts @@ -33,7 +33,7 @@ export const sortFilmSimulationsWithCount = ( export const titleForFilmSimulation = ( simulation: FilmSimulation, - photos:Photo[], + photos: Photo[], explicitCount?: number, ) => [ labelForFilmSimulation(simulation).large, diff --git a/src/site/Footer.tsx b/src/site/Footer.tsx index 3572d60d..7daa878d 100644 --- a/src/site/Footer.tsx +++ b/src/site/Footer.tsx @@ -7,7 +7,7 @@ import Link from 'next/link'; import { SHOW_REPO_LINK } from '@/site/config'; import RepoLink from '../components/RepoLink'; import { usePathname } from 'next/navigation'; -import { isPathAdmin, isPathSignIn, pathForAdminPhotos } from './paths'; +import { PATH_ADMIN_PHOTOS, isPathAdmin, isPathSignIn } from './paths'; import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus'; import { signOutAndRedirectAction } from '@/auth/actions'; import Spinner from '@/components/Spinner'; @@ -57,7 +57,7 @@ export default function Footer() { } : <> - + Admin {SHOW_REPO_LINK && diff --git a/src/site/paths.ts b/src/site/paths.ts index 9996ac6f..60a8da35 100644 --- a/src/site/paths.ts +++ b/src/site/paths.ts @@ -6,24 +6,26 @@ import { parameterize } from '@/utility/string'; import { TAG_HIDDEN } from '@/tag'; // Core paths -export const PATH_ROOT = '/'; -export const PATH_GRID = '/grid'; -export const PATH_ADMIN = '/admin'; -export const PATH_API = '/api'; -export const PATH_SIGN_IN = '/sign-in'; -export const PATH_OG = '/og'; +export const PATH_ROOT = '/'; +export const PATH_GRID = '/grid'; +export const PATH_ADMIN = '/admin'; +export const PATH_API = '/api'; +export const PATH_SIGN_IN = '/sign-in'; +export const PATH_OG = '/og'; // Path prefixes export const PREFIX_PHOTO = '/p'; export const PREFIX_TAG = '/tag'; export const PREFIX_CAMERA = '/shot-on'; export const PREFIX_FILM_SIMULATION = '/film'; +export const PREFIX_FOCAL_LENGTH = '/focal'; // Dynamic paths const PATH_PHOTO_DYNAMIC = `${PREFIX_PHOTO}/[photoId]`; const PATH_TAG_DYNAMIC = `${PREFIX_TAG}/[tag]`; const PATH_CAMERA_DYNAMIC = `${PREFIX_CAMERA}/[make]/[model]`; const PATH_FILM_SIMULATION_DYNAMIC = `${PREFIX_FILM_SIMULATION}/[simulation]`; +const PATH_FOCAL_LENGTH_DYNAMIC = `${PREFIX_FOCAL_LENGTH}/[focal]`; // Admin paths export const PATH_ADMIN_PHOTOS = `${PATH_ADMIN}/photos`; @@ -39,7 +41,6 @@ export const PATH_API_PRESIGNED_URL = `${PATH_API_STORAGE}/presigned-url`; // Modifiers const SHARE = 'share'; -const NEXT = 'next'; const EDIT = 'edit'; export const PATHS_ADMIN = [ @@ -58,24 +59,13 @@ export const PATHS_TO_CACHE = [ PATH_TAG_DYNAMIC, PATH_CAMERA_DYNAMIC, PATH_FILM_SIMULATION_DYNAMIC, + PATH_FOCAL_LENGTH_DYNAMIC, ...PATHS_ADMIN, ]; // Absolute paths export const ABSOLUTE_PATH_FOR_HOME_IMAGE = `${BASE_URL}/home-image`; -const pathWithNext = (path: string, next?: number) => - next !== undefined ? `${path}?${NEXT}=${next}` : path; - -export const pathForRoot = (next?: number) => - pathWithNext(PATH_ROOT, next); - -export const pathForGrid = (next?: number) => - pathWithNext(PATH_GRID, next); - -export const pathForAdminPhotos = (next?: number) => - pathWithNext(PATH_ADMIN_PHOTOS, next); - export const pathForAdminUploadUrl = (url: string) => `${PATH_ADMIN_UPLOADS}/${encodeURIComponent(url)}`; @@ -85,9 +75,6 @@ export const pathForAdminPhotoEdit = (photo: PhotoOrPhotoId) => export const pathForAdminTagEdit = (tag: string) => `${PATH_ADMIN_TAGS}/${tag}/${EDIT}`; -export const pathForOg = (next?: number) => - pathWithNext(PATH_OG, next); - type PhotoOrPhotoId = Photo | string; const getPhotoId = (photoOrPhotoId: PhotoOrPhotoId) => @@ -98,6 +85,7 @@ export const pathForPhoto = ( tag?: string, camera?: Camera, simulation?: FilmSimulation, + focal?: number, ) => typeof photo !== 'string' && photo.hidden ? `${pathForTag(TAG_HIDDEN)}/${getPhotoId(photo)}` @@ -107,7 +95,9 @@ export const pathForPhoto = ( ? `${pathForCamera(camera)}/${getPhotoId(photo)}` : simulation ? `${pathForFilmSimulation(simulation)}/${getPhotoId(photo)}` - : `${PREFIX_PHOTO}/${getPhotoId(photo)}`; + : focal + ? `${pathForFocalLength(focal)}/${getPhotoId(photo)}` + : `${PREFIX_PHOTO}/${getPhotoId(photo)}`; export const pathForPhotoShare = ( photo: PhotoOrPhotoId, @@ -117,30 +107,23 @@ export const pathForPhotoShare = ( ) => `${pathForPhoto(photo, tag, camera, simulation)}/${SHARE}`; -export const pathForTag = (tag: string, next?: number) => - pathWithNext( - `${PREFIX_TAG}/${tag}`, - next, - ); +export const pathForTag = (tag: string) => + `${PREFIX_TAG}/${tag}`; export const pathForTagShare = (tag: string) => `${pathForTag(tag)}/${SHARE}`; -export const pathForCamera = ({ make, model }: Camera, next?: number) => - pathWithNext( - `${PREFIX_CAMERA}/${parameterize(make, true)}/${parameterize(model, true)}`, - next, - ); +export const pathForCamera = ({ make, model }: Camera) => + `${PREFIX_CAMERA}/${parameterize(make, true)}/${parameterize(model, true)}`; export const pathForCameraShare = (camera: Camera) => `${pathForCamera(camera)}/${SHARE}`; -export const pathForFilmSimulation = - (simulation: FilmSimulation, next?: number) => - pathWithNext( - `${PREFIX_FILM_SIMULATION}/${simulation}`, - next, - ); +export const pathForFilmSimulation = (simulation: FilmSimulation) => + `${PREFIX_FILM_SIMULATION}/${simulation}`; + +export const pathForFocalLength = (focal: number) => + `${PREFIX_FOCAL_LENGTH}/${focal}mm`; export const pathForFilmSimulationShare = (simulation: FilmSimulation) => `${pathForFilmSimulation(simulation)}/${SHARE}`; @@ -162,6 +145,9 @@ export const absolutePathForCamera= (camera: Camera) => export const absolutePathForFilmSimulation = (simulation: FilmSimulation) => `${BASE_URL}${pathForFilmSimulation(simulation)}`; +export const absolutePathForFocalLength = (focal: number) => + `${BASE_URL}${pathForFocalLength(focal)}`; + export const absolutePathForPhotoImage = (photo: PhotoOrPhotoId) => `${absolutePathForPhoto(photo)}/image`; @@ -175,6 +161,10 @@ export const absolutePathForFilmSimulationImage = (simulation: FilmSimulation) => `${absolutePathForFilmSimulation(simulation)}/image`; +export const absolutePathForFocalLengthImage = + (focal: number) => + `${absolutePathForFocalLength(focal)}/image`; + // p/[photoId] export const isPathPhoto = (pathname = '') => new RegExp(`^${PREFIX_PHOTO}/[^/]+/?$`).test(pathname); @@ -232,6 +222,23 @@ export const isPathFilmSimulationPhotoShare = (pathname = '') => new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/[^/]+/${SHARE}/?$`) .test(pathname); +// focal/[focal] +export const isPathFocalLength = (pathname = '') => + new RegExp(`^${PREFIX_FOCAL_LENGTH}/[^/]+/?$`).test(pathname); + +// focal/[focal]/share +export const isPathFocalLengthShare = (pathname = '') => + new RegExp(`^${PREFIX_FOCAL_LENGTH}/[^/]+/${SHARE}/?$`).test(pathname); + +// focal/[focal]/[photoId] +export const isPathFocalLengthPhoto = (pathname = '') => + new RegExp(`^${PREFIX_FOCAL_LENGTH}/[^/]+/[^/]+/?$`).test(pathname); + +// focal/[focal]/[photoId]/share +export const isPathFocalLengthPhotoShare = (pathname = '') => + new RegExp(`^${PREFIX_FOCAL_LENGTH}/[^/]+/[^/]+/${SHARE}/?$`) + .test(pathname); + export const checkPathPrefix = (pathname = '', prefix: string) => pathname.toLowerCase().startsWith(prefix); @@ -260,6 +267,7 @@ export const getPathComponents = (pathname = ''): { tag?: string camera?: Camera simulation?: FilmSimulation + focal?: number } => { const photoIdFromPhoto = pathname.match( new RegExp(`^${PREFIX_PHOTO}/([^/]+)`))?.[1]; @@ -269,6 +277,8 @@ export const getPathComponents = (pathname = ''): { new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/((?!${SHARE})[^/]+)`))?.[1]; const photoIdFromFilmSimulation = pathname.match( new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/((?!${SHARE})[^/]+)`))?.[1]; + const photoIdFromFocalLength = pathname.match( + new RegExp(`^${PREFIX_FOCAL_LENGTH}/[0-9]+mm/((?!${SHARE})[^/]+)`))?.[1]; const tag = pathname.match( new RegExp(`^${PREFIX_TAG}/([^/]+)`))?.[1]; const cameraMake = pathname.match( @@ -277,31 +287,45 @@ export const getPathComponents = (pathname = ''): { new RegExp(`^${PREFIX_CAMERA}/[^/]+/([^/]+)`))?.[1]; const simulation = pathname.match( new RegExp(`^${PREFIX_FILM_SIMULATION}/([^/]+)`))?.[1] as FilmSimulation; + const focalString = pathname.match( + new RegExp(`^${PREFIX_FOCAL_LENGTH}/([0-9]+)mm`))?.[1]; const camera = cameraMake && cameraModel ? { make: cameraMake, model: cameraModel } : undefined; + const focal = focalString ? parseInt(focalString) : undefined; + return { photoId: ( photoIdFromPhoto || photoIdFromTag || photoIdFromCamera || - photoIdFromFilmSimulation + photoIdFromFilmSimulation || + photoIdFromFocalLength ), tag, camera, simulation, + focal, }; }; export const getEscapePath = (pathname?: string) => { - const { photoId, tag, camera, simulation } = getPathComponents(pathname); + const { + photoId, + tag, + camera, + simulation, + focal, + } = getPathComponents(pathname); + if ( (photoId && isPathPhoto(pathname)) || (tag && isPathTag(pathname)) || (camera && isPathCamera(pathname)) || - (simulation && isPathFilmSimulation(pathname)) + (simulation && isPathFilmSimulation(pathname)) || + (focal && isPathFocalLength(pathname)) ) { return PATH_GRID; } else if (photoId && isPathTagPhotoShare(pathname)) { @@ -310,6 +334,8 @@ export const getEscapePath = (pathname?: string) => { return pathForPhoto(photoId, undefined, camera); } else if (photoId && isPathFilmSimulationPhotoShare(pathname)) { return pathForPhoto(photoId, undefined, undefined, simulation); + } else if (photoId && isPathFocalLengthPhotoShare(pathname)) { + return pathForPhoto(photoId, undefined, undefined, undefined, focal); } else if (photoId && isPathPhotoShare(pathname)) { return pathForPhoto(photoId); } else if (tag && ( @@ -327,5 +353,10 @@ export const getEscapePath = (pathname?: string) => { isPathFilmSimulationShare(pathname) )) { return pathForFilmSimulation(simulation); + } else if (focal && ( + isPathFocalLengthPhoto(pathname) || + isPathFocalLengthShare(pathname) + )) { + return pathForFocalLength(focal); } }; From 393ec17f844a9c675b36cf6b7603aed972217d11 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 21 May 2024 00:30:28 -0500 Subject: [PATCH 2/6] Disable sql debugging --- src/site/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/site/config.ts b/src/site/config.ts index 9c02ed4b..e119321a 100644 --- a/src/site/config.ts +++ b/src/site/config.ts @@ -5,7 +5,7 @@ import { makeUrlAbsolute, shortenUrl } from '@/utility/url'; // HARD-CODED GLOBAL CONFIGURATION export const SHOULD_PREFETCH_ALL_LINKS: boolean | undefined = undefined; -export const SHOULD_DEBUG_SQL = true; +export const SHOULD_DEBUG_SQL = false; // META / DOMAINS From 7cd5ccbe1537c16b2d88c04bea2b316090d86f5d Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 21 May 2024 12:03:32 -0500 Subject: [PATCH 3/6] Add core focal length views --- src/admin/AdminPhotoMenuClient.tsx | 2 +- src/admin/AdminPhotosTable.tsx | 2 +- .../film/[simulation]/[photoId]/layout.tsx | 2 +- src/app/focal/[focal]/image/route.tsx | 41 +++++++++++ src/app/focal/[focal]/page.tsx | 71 +++++++++++++++++++ src/app/p/[photoId]/layout.tsx | 2 +- .../[make]/[model]/[photoId]/layout.tsx | 7 +- src/app/tag/[tag]/[photoId]/layout.tsx | 2 +- src/app/tag/hidden/[photoId]/page.tsx | 2 +- src/components/CommandKClient.tsx | 2 +- src/focal/FocalLengthHeader.tsx | 39 ++++++++++ src/focal/FocalLengthOverview.tsx | 33 +++++++++ src/focal/PhotoFocalLength.tsx | 31 ++++++++ src/focal/data.ts | 17 +++++ src/focal/index.ts | 11 ++- .../FocalLengthImageResponse.tsx | 46 ++++++++++++ src/photo/PhotoGrid.tsx | 3 + src/photo/PhotoGridInfinite.tsx | 3 + src/photo/PhotoGridPage.tsx | 4 ++ src/photo/PhotoLarge.tsx | 16 +++-- src/photo/PhotoLink.tsx | 4 +- src/photo/PhotoLinks.tsx | 19 ++++- src/photo/PhotoMedium.tsx | 4 +- src/photo/PhotoOGTile.tsx | 2 +- src/photo/PhotoShareModal.tsx | 14 ++-- src/photo/PhotoSmall.tsx | 10 ++- src/photo/actions.ts | 2 +- src/photo/cache.ts | 2 +- src/photo/db/index.ts | 26 ++++--- src/photo/index.ts | 2 +- src/site/api.ts | 2 +- src/site/paths.ts | 52 +++++++------- src/utility/exif.ts | 3 - 33 files changed, 401 insertions(+), 77 deletions(-) create mode 100644 src/app/focal/[focal]/image/route.tsx create mode 100644 src/app/focal/[focal]/page.tsx create mode 100644 src/focal/FocalLengthHeader.tsx create mode 100644 src/focal/FocalLengthOverview.tsx create mode 100644 src/focal/PhotoFocalLength.tsx create mode 100644 src/focal/data.ts create mode 100644 src/image-response/FocalLengthImageResponse.tsx diff --git a/src/admin/AdminPhotoMenuClient.tsx b/src/admin/AdminPhotoMenuClient.tsx index 60b902a7..ee792f59 100644 --- a/src/admin/AdminPhotoMenuClient.tsx +++ b/src/admin/AdminPhotoMenuClient.tsx @@ -25,7 +25,7 @@ export default function AdminPhotoMenuClient({ const isFav = isPhotoFav(photo); const path = usePathname(); const shouldRedirectFav = isPathFavs(path) && isFav; - const shouldRedirectDelete = pathForPhoto(photo.id) === path; + const shouldRedirectDelete = pathForPhoto({ photo: photo.id }) === path; return ( isUserSignedIn diff --git a/src/admin/AdminPhotosTable.tsx b/src/admin/AdminPhotosTable.tsx index 34dcf7de..15d6c6fc 100644 --- a/src/admin/AdminPhotosTable.tsx +++ b/src/admin/AdminPhotosTable.tsx @@ -45,7 +45,7 @@ export default function AdminPhotosTable({
diff --git a/src/app/film/[simulation]/[photoId]/layout.tsx b/src/app/film/[simulation]/[photoId]/layout.tsx index 4773e8da..4219e838 100644 --- a/src/app/film/[simulation]/[photoId]/layout.tsx +++ b/src/app/film/[simulation]/[photoId]/layout.tsx @@ -41,7 +41,7 @@ export async function generateMetadata({ const title = titleForPhoto(photo); const description = descriptionForPhoto(photo); const images = absolutePathForPhotoImage(photo); - const url = absolutePathForPhoto(photo, simulation); + const url = absolutePathForPhoto({ photo, simulation }); return { title, diff --git a/src/app/focal/[focal]/image/route.tsx b/src/app/focal/[focal]/image/route.tsx new file mode 100644 index 00000000..22db2d06 --- /dev/null +++ b/src/app/focal/[focal]/image/route.tsx @@ -0,0 +1,41 @@ +import { getPhotosCached } from '@/photo/cache'; +import { + IMAGE_OG_DIMENSION_SMALL, + MAX_PHOTOS_TO_SHOW_PER_TAG, +} from '@/image-response'; +import { getIBMPlexMonoMedium } from '@/site/font'; +import { ImageResponse } from 'next/og'; +import { getImageResponseCacheControlHeaders } from '@/image-response/cache'; +import FocalLengthImageResponse from + '@/image-response/FocalLengthImageResponse'; +import { getFocalLengthFromString } from '@/focal'; + +export async function GET( + _: Request, + context: { params: { focal: string } }, +) { + const focal = getFocalLengthFromString(context.params.focal); + + const [ + photos, + { fontFamily, fonts }, + headers, + ] = await Promise.all([ + getPhotosCached({ limit: MAX_PHOTOS_TO_SHOW_PER_TAG, focal }), + getIBMPlexMonoMedium(), + getImageResponseCacheControlHeaders(), + ]); + + const { width, height } = IMAGE_OG_DIMENSION_SMALL; + + return new ImageResponse( + , + { width, height, fonts, headers }, + ); +} diff --git a/src/app/focal/[focal]/page.tsx b/src/app/focal/[focal]/page.tsx new file mode 100644 index 00000000..f8b01767 --- /dev/null +++ b/src/app/focal/[focal]/page.tsx @@ -0,0 +1,71 @@ +import { generateMetaForFocalLength, getFocalLengthFromString } from '@/focal'; +import FocalLengthOverview from '@/focal/FocalLengthOverview'; +import { getPhotosFocalDataCached } from '@/focal/data'; +import { INFINITE_SCROLL_GRID_PHOTO_INITIAL } from '@/photo'; +import { PATH_ROOT } from '@/site/paths'; +import type { Metadata } from 'next'; +import { redirect } from 'next/navigation'; +import { cache } from 'react'; + +const getPhotosFocalDataCachedCached = cache((focal: number) => + getPhotosFocalDataCached({ + focal, + limit: INFINITE_SCROLL_GRID_PHOTO_INITIAL, + })); + +interface FocalLengthProps { + params: { focal: string } +} + +export async function generateMetadata({ + params: { focal: focalString }, +}: FocalLengthProps): Promise { + const focal = getFocalLengthFromString(focalString); + + const [ + photos, + { count, dateRange }, + ] = await getPhotosFocalDataCachedCached(focal); + + if (photos.length === 0) { return {}; } + + const { + url, + title, + description, + images, + } = generateMetaForFocalLength(focal, photos, count, dateRange); + + return { + title, + openGraph: { + title, + description, + images, + url, + }, + twitter: { + images, + description, + card: 'summary_large_image', + }, + description, + }; +} + +export default async function TagPage({ + params: { focal: focalString }, +}:FocalLengthProps) { + const focal = getFocalLengthFromString(focalString); + + const [ + photos, + { count, dateRange }, + ] = await getPhotosFocalDataCachedCached(focal); + + if (photos.length === 0) { redirect(PATH_ROOT); } + + return ( + + ); +} diff --git a/src/app/p/[photoId]/layout.tsx b/src/app/p/[photoId]/layout.tsx index e40e7d9c..1a00868a 100644 --- a/src/app/p/[photoId]/layout.tsx +++ b/src/app/p/[photoId]/layout.tsx @@ -44,7 +44,7 @@ export async function generateMetadata({ const title = titleForPhoto(photo); const description = descriptionForPhoto(photo); const images = absolutePathForPhotoImage(photo); - const url = absolutePathForPhoto(photo); + const url = absolutePathForPhoto({ photo }); return { title, diff --git a/src/app/shot-on/[make]/[model]/[photoId]/layout.tsx b/src/app/shot-on/[make]/[model]/[photoId]/layout.tsx index 42efcca0..b2908928 100644 --- a/src/app/shot-on/[make]/[model]/[photoId]/layout.tsx +++ b/src/app/shot-on/[make]/[model]/[photoId]/layout.tsx @@ -44,11 +44,10 @@ export async function generateMetadata({ const title = titleForPhoto(photo); const description = descriptionForPhoto(photo); const images = absolutePathForPhotoImage(photo); - const url = absolutePathForPhoto( + const url = absolutePathForPhoto({ photo, - undefined, - cameraFromPhoto(photo, { make, model }), - ); + camera: cameraFromPhoto(photo, { make, model }), + }); return { title, diff --git a/src/app/tag/[tag]/[photoId]/layout.tsx b/src/app/tag/[tag]/[photoId]/layout.tsx index 5de147fd..cb8ebd85 100644 --- a/src/app/tag/[tag]/[photoId]/layout.tsx +++ b/src/app/tag/[tag]/[photoId]/layout.tsx @@ -35,7 +35,7 @@ export async function generateMetadata({ const title = titleForPhoto(photo); const description = descriptionForPhoto(photo); const images = absolutePathForPhotoImage(photo); - const url = absolutePathForPhoto(photo, tag); + const url = absolutePathForPhoto({ photo, tag }); return { title, diff --git a/src/app/tag/hidden/[photoId]/page.tsx b/src/app/tag/hidden/[photoId]/page.tsx index 954fca0c..796447c0 100644 --- a/src/app/tag/hidden/[photoId]/page.tsx +++ b/src/app/tag/hidden/[photoId]/page.tsx @@ -33,7 +33,7 @@ export async function generateMetadata({ const title = titleForPhoto(photo); const description = descriptionForPhoto(photo); - const url = absolutePathForPhoto(photo, TAG_HIDDEN); + const url = absolutePathForPhoto({ photo, tag: TAG_HIDDEN }); return { title, diff --git a/src/components/CommandKClient.tsx b/src/components/CommandKClient.tsx index 4f5af280..61d6bd10 100644 --- a/src/components/CommandKClient.tsx +++ b/src/components/CommandKClient.tsx @@ -147,7 +147,7 @@ export default function CommandKClient({ keywords: getKeywordsForPhoto(photo), annotation: , accessory: , - path: pathForPhoto(photo), + path: pathForPhoto({ photo }), })), }] : []); diff --git a/src/focal/FocalLengthHeader.tsx b/src/focal/FocalLengthHeader.tsx new file mode 100644 index 00000000..9babb486 --- /dev/null +++ b/src/focal/FocalLengthHeader.tsx @@ -0,0 +1,39 @@ +import { Photo, PhotoDateRange } from '@/photo'; +import { descriptionForFocalLengthPhotos } from '.'; +import { pathForFocalLength } from '@/site/paths'; +import PhotoSetHeader from '@/photo/PhotoSetHeader'; +import PhotoFocalLength from './PhotoFocalLength'; + +export default function FocalLengthHeader({ + focal, + photos, + selectedPhoto, + indexNumber, + count, + dateRange, +}: { + focal: number + photos: Photo[] + selectedPhoto?: Photo + indexNumber?: number + count?: number + dateRange?: PhotoDateRange +}) { + return ( + } + entityVerb="Tagged" + entityDescription={descriptionForFocalLengthPhotos( + photos, + undefined, + count, + )} + photos={photos} + selectedPhoto={selectedPhoto} + sharePath={pathForFocalLength(focal)} + indexNumber={indexNumber} + count={count} + dateRange={dateRange} + /> + ); +} diff --git a/src/focal/FocalLengthOverview.tsx b/src/focal/FocalLengthOverview.tsx new file mode 100644 index 00000000..a5d23463 --- /dev/null +++ b/src/focal/FocalLengthOverview.tsx @@ -0,0 +1,33 @@ +import { Photo, PhotoDateRange } from '@/photo'; +import PhotoGridPage from '@/photo/PhotoGridPage'; +import FocalLengthHeader from './FocalLengthHeader'; + +export default function FocalLengthOverview({ + focal, + photos, + count, + dateRange, + animateOnFirstLoadOnly, +}: { + focal: number, + photos: Photo[], + count: number, + dateRange?: PhotoDateRange, + animateOnFirstLoadOnly?: boolean, +}) { + return ( + , + animateOnFirstLoadOnly, + }} /> + ); +} diff --git a/src/focal/PhotoFocalLength.tsx b/src/focal/PhotoFocalLength.tsx new file mode 100644 index 00000000..b242e43b --- /dev/null +++ b/src/focal/PhotoFocalLength.tsx @@ -0,0 +1,31 @@ +import { pathForFocalLength } from '@/site/paths'; +import EntityLink, { + EntityLinkExternalProps, +} from '@/components/primitives/EntityLink'; +import { TbCone } from 'react-icons/tb'; +import { formatFocalLength } from '.'; + +export default function PhotoFocalLength({ + focal, + type, + badged, + contrast, + prefetch, + countOnHover, +}: { + focal: number + countOnHover?: number +} & EntityLinkExternalProps) { + return ( + } + type={type} + badged={badged} + contrast={contrast} + prefetch={prefetch} + hoverEntity={countOnHover} + /> + ); +} diff --git a/src/focal/data.ts b/src/focal/data.ts new file mode 100644 index 00000000..be5b539d --- /dev/null +++ b/src/focal/data.ts @@ -0,0 +1,17 @@ +import { + getPhotosCached, + getPhotosMetaCached, +} from '@/photo/cache'; + +export const getPhotosFocalDataCached = ({ + focal, + limit, +}: { + focal: number, + limit?: number, +}) => + Promise.all([ + getPhotosCached({ focal, limit }), + getPhotosMetaCached({ focal }), + ]); + diff --git a/src/focal/index.ts b/src/focal/index.ts index 712e8a25..9104df94 100644 --- a/src/focal/index.ts +++ b/src/focal/index.ts @@ -9,12 +9,21 @@ import { absolutePathForFocalLengthImage, } from '@/site/paths'; +export const getFocalLengthFromString = (focalString?: string) => { + const focal = focalString?.match(/^([0-9]+)mm/)?.[1]; + return focal ? parseInt(focal, 10) : 0; +}; + +export const formatFocalLength = (focal?: number) => focal ? + `${focal}mm` + : undefined; + export const titleForFocalLength = ( focal: number, photos: Photo[], explicitCount?: number, ) => [ - `${focal}mm`, + formatFocalLength(focal), photoQuantityText(explicitCount ?? photos.length), ].join(' '); diff --git a/src/image-response/FocalLengthImageResponse.tsx b/src/image-response/FocalLengthImageResponse.tsx new file mode 100644 index 00000000..498f9cfd --- /dev/null +++ b/src/image-response/FocalLengthImageResponse.tsx @@ -0,0 +1,46 @@ +import type { Photo } from '../photo'; +import ImageCaption from './components/ImageCaption'; +import ImagePhotoGrid from './components/ImagePhotoGrid'; +import ImageContainer from './components/ImageContainer'; +import type { NextImageSize } from '@/services/next-image'; +import { TbCone } from 'react-icons/tb'; +import { formatFocalLength } from '@/focal'; + +export default function FocalLengthImageResponse({ + focal, + photos, + width, + height, + fontFamily, +}: { + focal: number, + photos: Photo[] + width: NextImageSize + height: number + fontFamily: string +}) { + return ( + + + + + {formatFocalLength(focal)} + + + ); +} diff --git a/src/photo/PhotoGrid.tsx b/src/photo/PhotoGrid.tsx index 32e669d4..6dbc57f4 100644 --- a/src/photo/PhotoGrid.tsx +++ b/src/photo/PhotoGrid.tsx @@ -12,6 +12,7 @@ export default function PhotoGrid({ tag, camera, simulation, + focal, photoPriority, fast, animate = true, @@ -28,6 +29,7 @@ export default function PhotoGrid({ tag?: string camera?: Camera simulation?: FilmSimulation + focal?: number photoPriority?: boolean fast?: boolean animate?: boolean @@ -77,6 +79,7 @@ export default function PhotoGrid({ tag, camera, simulation, + focal, selected: photo.id === selectedPhoto?.id, priority: photoPriority, onVisible: index === photos.length - 1 diff --git a/src/photo/PhotoGridInfinite.tsx b/src/photo/PhotoGridInfinite.tsx index 98b43667..0b9a1428 100644 --- a/src/photo/PhotoGridInfinite.tsx +++ b/src/photo/PhotoGridInfinite.tsx @@ -13,6 +13,7 @@ export default function PhotoGridInfinite({ tag, camera, simulation, + focal, animateOnFirstLoadOnly, }: { cacheKey: string @@ -21,6 +22,7 @@ export default function PhotoGridInfinite({ tag?: string camera?: Camera simulation?: FilmSimulation + focal?: number animateOnFirstLoadOnly?: boolean }) { return ( @@ -39,6 +41,7 @@ export default function PhotoGridInfinite({ tag, camera, simulation, + focal, onLastPhotoVisible, animateOnFirstLoadOnly, }} />} diff --git a/src/photo/PhotoGridPage.tsx b/src/photo/PhotoGridPage.tsx index 4e1fdc74..eb3430f0 100644 --- a/src/photo/PhotoGridPage.tsx +++ b/src/photo/PhotoGridPage.tsx @@ -17,6 +17,7 @@ export default function PhotoGridPage({ tag, camera, simulation, + focal, animateOnFirstLoadOnly, header, sidebar, @@ -27,6 +28,7 @@ export default function PhotoGridPage({ tag?: string camera?: Camera simulation?: FilmSimulation + focal?: number animateOnFirstLoadOnly?: boolean header?: JSX.Element sidebar?: JSX.Element @@ -58,6 +60,7 @@ export default function PhotoGridPage({ tag, camera, simulation, + focal, animateOnFirstLoadOnly, onAnimationComplete, }} /> @@ -69,6 +72,7 @@ export default function PhotoGridPage({ tag, camera, simulation, + focal, animateOnFirstLoadOnly, }} />}
diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index 92862170..e7e7c561 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -40,6 +40,7 @@ export default function PhotoLarge({ shouldShareTag, shouldShareCamera, shouldShareSimulation, + shouldShareFocalLength, shouldScrollOnShare, onVisible, }: { @@ -54,6 +55,7 @@ export default function PhotoLarge({ shouldShareTag?: boolean shouldShareCamera?: boolean shouldShareSimulation?: boolean + shouldShareFocalLength?: boolean shouldScrollOnShare?: boolean onVisible?: () => void }) { @@ -76,7 +78,7 @@ export default function PhotoLarge({ containerRef={ref} contentMain={ diff --git a/src/photo/PhotoLink.tsx b/src/photo/PhotoLink.tsx index bbdf02d3..34bcca38 100644 --- a/src/photo/PhotoLink.tsx +++ b/src/photo/PhotoLink.tsx @@ -15,6 +15,7 @@ export default function PhotoLink({ tag, camera, simulation, + focal, scroll, prefetch, nextPhotoAnimation, @@ -25,6 +26,7 @@ export default function PhotoLink({ tag?: string camera?: Camera simulation?: FilmSimulation + focal?: number scroll?: boolean prefetch?: boolean nextPhotoAnimation?: AnimationConfig @@ -36,7 +38,7 @@ export default function PhotoLink({ return ( photo ? { if (nextPhotoAnimation) { diff --git a/src/photo/PhotoLinks.tsx b/src/photo/PhotoLinks.tsx index 0c369464..6a7a5c1b 100644 --- a/src/photo/PhotoLinks.tsx +++ b/src/photo/PhotoLinks.tsx @@ -21,12 +21,14 @@ export default function PhotoLinks({ tag, camera, simulation, + focal, }: { photo: Photo photos: Photo[] tag?: string camera?: Camera simulation?: FilmSimulation + focal?: number }) { const router = useRouter(); @@ -47,7 +49,13 @@ export default function PhotoLinks({ if (previousPhoto) { setNextPhotoAnimation?.(ANIMATION_RIGHT); router.push( - pathForPhoto(previousPhoto, tag, camera, simulation), + pathForPhoto({ + photo: previousPhoto, + tag, + camera, + simulation, + focal, + }), { scroll: false }, ); } @@ -57,7 +65,13 @@ export default function PhotoLinks({ if (nextPhoto) { setNextPhotoAnimation?.(ANIMATION_LEFT); router.push( - pathForPhoto(nextPhoto, tag, camera, simulation), + pathForPhoto({ + photo: nextPhoto, + tag, + camera, + simulation, + focal, + }), { scroll: false }, ); } @@ -76,6 +90,7 @@ export default function PhotoLinks({ tag, camera, simulation, + focal, ]); return ( diff --git a/src/photo/PhotoMedium.tsx b/src/photo/PhotoMedium.tsx index ec666919..72fdd0a4 100644 --- a/src/photo/PhotoMedium.tsx +++ b/src/photo/PhotoMedium.tsx @@ -16,6 +16,7 @@ export default function PhotoMedium({ tag, camera, simulation, + focal, selected, priority, prefetch = SHOULD_PREFETCH_ALL_LINKS, @@ -26,6 +27,7 @@ export default function PhotoMedium({ tag?: string camera?: Camera simulation?: FilmSimulation + focal?: number selected?: boolean priority?: boolean prefetch?: boolean @@ -39,7 +41,7 @@ export default function PhotoMedium({ return ( - + ); }; diff --git a/src/photo/PhotoSmall.tsx b/src/photo/PhotoSmall.tsx index 36ef31ee..66a9819a 100644 --- a/src/photo/PhotoSmall.tsx +++ b/src/photo/PhotoSmall.tsx @@ -6,10 +6,15 @@ import { pathForPhoto } from '@/site/paths'; import { SHOULD_PREFETCH_ALL_LINKS } from '@/site/config'; import { useRef } from 'react'; import useOnVisible from '@/utility/useOnVisible'; +import { Camera } from '@/camera'; +import { FilmSimulation } from '@/simulation'; export default function PhotoSmall({ photo, tag, + camera, + simulation, + focal, selected, className, prefetch = SHOULD_PREFETCH_ALL_LINKS, @@ -17,6 +22,9 @@ export default function PhotoSmall({ }: { photo: Photo tag?: string + camera?: Camera + simulation?: FilmSimulation + focal?: number selected?: boolean className?: string prefetch?: boolean @@ -29,7 +37,7 @@ export default function PhotoSmall({ return ( { revalidateCamerasKey(); revalidateFilmSimulationsKey(); // Paths - revalidatePath(pathForPhoto(photoId), 'layout'); + revalidatePath(pathForPhoto({ photo: photoId }), 'layout'); revalidatePath(PATH_ROOT, 'layout'); revalidatePath(PATH_GRID, 'layout'); revalidatePath(PREFIX_TAG, 'layout'); diff --git a/src/photo/db/index.ts b/src/photo/db/index.ts index 8094d545..79697d6c 100644 --- a/src/photo/db/index.ts +++ b/src/photo/db/index.ts @@ -7,16 +7,17 @@ export const GENERATE_STATIC_PARAMS_LIMIT = 1000; export const PHOTO_DEFAULT_LIMIT = 100; export type GetPhotosOptions = { - sortBy?: 'createdAt' | 'takenAt' | 'priority'; - limit?: number; - offset?: number; - query?: string; - tag?: string; - camera?: Camera; - simulation?: FilmSimulation; - takenBefore?: Date; - takenAfterInclusive?: Date; - hidden?: 'exclude' | 'include' | 'only'; + sortBy?: 'createdAt' | 'takenAt' | 'priority' + limit?: number + offset?: number + query?: string + tag?: string + camera?: Camera + simulation?: FilmSimulation + focal?: number + takenBefore?: Date + takenAfterInclusive?: Date + hidden?: 'exclude' | 'include' | 'only' }; export const getWheresFromOptions = ( @@ -31,6 +32,7 @@ export const getWheresFromOptions = ( tag, camera, simulation, + focal, } = options; const wheres = [] as string[]; @@ -73,6 +75,10 @@ export const getWheresFromOptions = ( wheres.push(`film_simulation=$${valuesIndex++}`); wheresValues.push(simulation); } + if (focal) { + wheres.push(`focal_length=$${valuesIndex++}`); + wheresValues.push(focal); + } return { wheres: wheres.length > 0 diff --git a/src/photo/index.ts b/src/photo/index.ts index c9ea8bef..01b0d0b0 100644 --- a/src/photo/index.ts +++ b/src/photo/index.ts @@ -1,3 +1,4 @@ +import { formatFocalLength } from '@/focal'; import { getNextImageUrlForRequest } from '@/services/next-image'; import { FilmSimulation } from '@/simulation'; import { HIGH_DENSITY_GRID, SHOW_EXIF_DATA } from '@/site/config'; @@ -8,7 +9,6 @@ import { formatIso, formatExposureCompensation, formatExposureTime, - formatFocalLength, } from '@/utility/exif'; import camelcaseKeys from 'camelcase-keys'; import { isBefore } from 'date-fns'; diff --git a/src/site/api.ts b/src/site/api.ts index b478d183..54ba5cbe 100644 --- a/src/site/api.ts +++ b/src/site/api.ts @@ -30,7 +30,7 @@ interface PublicApiPhoto { export const formatPhotoForApi = (photo: Photo): PublicApiPhoto => ({ id: photo.id, title: photo.title, - url: absolutePathForPhoto(photo), + url: absolutePathForPhoto({ photo }), ...photo.make && { make: photo.make }, ...photo.model && { model: photo.model }, ...photo.tags.length > 0 && { tags: photo.tags }, diff --git a/src/site/paths.ts b/src/site/paths.ts index 60a8da35..a04b4335 100644 --- a/src/site/paths.ts +++ b/src/site/paths.ts @@ -63,6 +63,14 @@ export const PATHS_TO_CACHE = [ ...PATHS_ADMIN, ]; +interface PhotoPathParams { + photo: PhotoOrPhotoId + tag?: string + camera?: Camera + simulation?: FilmSimulation + focal?: number +} + // Absolute paths export const ABSOLUTE_PATH_FOR_HOME_IMAGE = `${BASE_URL}/home-image`; @@ -80,13 +88,13 @@ type PhotoOrPhotoId = Photo | string; const getPhotoId = (photoOrPhotoId: PhotoOrPhotoId) => typeof photoOrPhotoId === 'string' ? photoOrPhotoId : photoOrPhotoId.id; -export const pathForPhoto = ( - photo: PhotoOrPhotoId, - tag?: string, - camera?: Camera, - simulation?: FilmSimulation, - focal?: number, -) => +export const pathForPhoto = ({ + photo, + tag, + camera, + simulation, + focal, +}: PhotoPathParams) => typeof photo !== 'string' && photo.hidden ? `${pathForTag(TAG_HIDDEN)}/${getPhotoId(photo)}` : tag @@ -99,13 +107,8 @@ export const pathForPhoto = ( ? `${pathForFocalLength(focal)}/${getPhotoId(photo)}` : `${PREFIX_PHOTO}/${getPhotoId(photo)}`; -export const pathForPhotoShare = ( - photo: PhotoOrPhotoId, - tag?: string, - camera?: Camera, - simulation?: FilmSimulation, -) => - `${pathForPhoto(photo, tag, camera, simulation)}/${SHARE}`; +export const pathForPhotoShare = (params: PhotoPathParams) => + `${pathForPhoto(params)}/${SHARE}`; export const pathForTag = (tag: string) => `${PREFIX_TAG}/${tag}`; @@ -128,13 +131,8 @@ export const pathForFocalLength = (focal: number) => export const pathForFilmSimulationShare = (simulation: FilmSimulation) => `${pathForFilmSimulation(simulation)}/${SHARE}`; -export const absolutePathForPhoto = ( - photo: PhotoOrPhotoId, - tag?: string, - camera?: Camera, - simulation?: FilmSimulation -) => - `${BASE_URL}${pathForPhoto(photo, tag, camera, simulation)}`; +export const absolutePathForPhoto = (params: PhotoPathParams) => + `${BASE_URL}${pathForPhoto(params)}`; export const absolutePathForTag = (tag: string) => `${BASE_URL}${pathForTag(tag)}`; @@ -149,7 +147,7 @@ export const absolutePathForFocalLength = (focal: number) => `${BASE_URL}${pathForFocalLength(focal)}`; export const absolutePathForPhotoImage = (photo: PhotoOrPhotoId) => - `${absolutePathForPhoto(photo)}/image`; + `${absolutePathForPhoto({ photo })}/image`; export const absolutePathForTagImage = (tag: string) => `${absolutePathForTag(tag)}/image`; @@ -329,15 +327,15 @@ export const getEscapePath = (pathname?: string) => { ) { return PATH_GRID; } else if (photoId && isPathTagPhotoShare(pathname)) { - return pathForPhoto(photoId, tag); + return pathForPhoto({ photo: photoId, tag }); } else if (photoId && isPathCameraPhotoShare(pathname)) { - return pathForPhoto(photoId, undefined, camera); + return pathForPhoto({ photo: photoId, camera }); } else if (photoId && isPathFilmSimulationPhotoShare(pathname)) { - return pathForPhoto(photoId, undefined, undefined, simulation); + return pathForPhoto({ photo: photoId, simulation }); } else if (photoId && isPathFocalLengthPhotoShare(pathname)) { - return pathForPhoto(photoId, undefined, undefined, undefined, focal); + return pathForPhoto({ photo: photoId, focal }); } else if (photoId && isPathPhotoShare(pathname)) { - return pathForPhoto(photoId); + return pathForPhoto({ photo: photoId }); } else if (tag && ( isPathTagPhoto(pathname) || isPathTagShare(pathname) diff --git a/src/utility/exif.ts b/src/utility/exif.ts index ebc36bf9..60b10288 100644 --- a/src/utility/exif.ts +++ b/src/utility/exif.ts @@ -31,9 +31,6 @@ export const getAspectRatioFromExif = (data: ExifData): number => { } }; -export const formatFocalLength = (focalLength?: number) => - focalLength ? `${focalLength}mm` : undefined; - export const formatAperture = (aperture?: number) => aperture ? `ƒ/${aperture}` : undefined; From f7321bd8315d1615e99e4d988ba8d237709e66d3 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 21 May 2024 12:34:45 -0500 Subject: [PATCH 4/6] Build out final focal length views --- src/app/focal/[focal]/[photoId]/layout.tsx | 86 +++++++++++++++++++ src/app/focal/[focal]/[photoId]/page.tsx | 3 + .../focal/[focal]/[photoId]/share/page.tsx | 19 ++++ src/app/focal/[focal]/page.tsx | 4 +- src/app/focal/[focal]/share/page.tsx | 70 +++++++++++++++ src/focal/FocalLengthHeader.tsx | 5 +- src/focal/FocalLengthOGTile.tsx | 50 +++++++++++ src/focal/FocalLengthShareModal.tsx | 26 ++++++ src/focal/data.ts | 2 +- src/focal/index.ts | 2 +- src/photo/PhotoDetailPage.tsx | 18 ++++ src/photo/PhotoLinks.tsx | 2 + src/site/paths.ts | 7 +- 13 files changed, 285 insertions(+), 9 deletions(-) create mode 100644 src/app/focal/[focal]/[photoId]/layout.tsx create mode 100644 src/app/focal/[focal]/[photoId]/page.tsx create mode 100644 src/app/focal/[focal]/[photoId]/share/page.tsx create mode 100644 src/app/focal/[focal]/share/page.tsx create mode 100644 src/focal/FocalLengthOGTile.tsx create mode 100644 src/focal/FocalLengthShareModal.tsx diff --git a/src/app/focal/[focal]/[photoId]/layout.tsx b/src/app/focal/[focal]/[photoId]/layout.tsx new file mode 100644 index 00000000..868d8c4d --- /dev/null +++ b/src/app/focal/[focal]/[photoId]/layout.tsx @@ -0,0 +1,86 @@ +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 '@/site/paths'; +import PhotoDetailPage from '@/photo/PhotoDetailPage'; +import { getPhotosNearIdCached } from '@/photo/cache'; +import { ReactNode, cache } from 'react'; +import { getPhotosMeta } from '@/photo/db/query'; +import { getFocalLengthFromString } from '@/focal'; + +const getPhotosNearIdCachedCached = cache((photoId: string, focal: number) => + getPhotosNearIdCached( + photoId, + { focal, limit: RELATED_GRID_PHOTOS_TO_SHOW + 2 }, + )); + +interface PhotoFocalLengthProps { + params: { photoId: string, focal: string } +} + +export async function generateMetadata({ + params: { photoId, focal: focalString }, +}: PhotoFocalLengthProps): Promise { + const focal = getFocalLengthFromString(focalString); + + const { photo } = await getPhotosNearIdCachedCached(photoId, focal); + + if (!photo) { return {}; } + + const title = titleForPhoto(photo); + const description = descriptionForPhoto(photo); + const images = absolutePathForPhotoImage(photo); + const url = absolutePathForPhoto({ photo, focal }); + + return { + title, + description, + openGraph: { + title, + images, + description, + url, + }, + twitter: { + title, + description, + images, + card: 'summary_large_image', + }, + }; +} + +export default async function PhotoFocalLengthPage({ + params: { photoId, focal: focalString }, + children, +}: PhotoFocalLengthProps & { children: ReactNode }) { + const focal = getFocalLengthFromString(focalString); + + const { photo, photos, photosGrid, indexNumber } = + await getPhotosNearIdCachedCached(photoId, focal); + + if (!photo) { redirect(PATH_ROOT); } + + const { count, dateRange } = await getPhotosMeta({ focal }); + + return <> + {children} + + ; +} diff --git a/src/app/focal/[focal]/[photoId]/page.tsx b/src/app/focal/[focal]/[photoId]/page.tsx new file mode 100644 index 00000000..67e08591 --- /dev/null +++ b/src/app/focal/[focal]/[photoId]/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return null; +} diff --git a/src/app/focal/[focal]/[photoId]/share/page.tsx b/src/app/focal/[focal]/[photoId]/share/page.tsx new file mode 100644 index 00000000..90702366 --- /dev/null +++ b/src/app/focal/[focal]/[photoId]/share/page.tsx @@ -0,0 +1,19 @@ +import { getFocalLengthFromString } from '@/focal'; +import { getPhotoCached } from '@/photo/cache'; +import PhotoShareModal from '@/photo/PhotoShareModal'; +import { PATH_ROOT } from '@/site/paths'; +import { redirect } from 'next/navigation'; + +export default async function Share({ + params: { photoId, focal: focalString }, +}: { + params: { photoId: string, focal: string } +}) { + const focal = getFocalLengthFromString(focalString); + + const photo = await getPhotoCached(photoId); + + if (!photo) { return redirect(PATH_ROOT); } + + return ; +} diff --git a/src/app/focal/[focal]/page.tsx b/src/app/focal/[focal]/page.tsx index f8b01767..82948194 100644 --- a/src/app/focal/[focal]/page.tsx +++ b/src/app/focal/[focal]/page.tsx @@ -1,6 +1,6 @@ import { generateMetaForFocalLength, getFocalLengthFromString } from '@/focal'; import FocalLengthOverview from '@/focal/FocalLengthOverview'; -import { getPhotosFocalDataCached } from '@/focal/data'; +import { getPhotosFocalLengthDataCached } from '@/focal/data'; import { INFINITE_SCROLL_GRID_PHOTO_INITIAL } from '@/photo'; import { PATH_ROOT } from '@/site/paths'; import type { Metadata } from 'next'; @@ -8,7 +8,7 @@ import { redirect } from 'next/navigation'; import { cache } from 'react'; const getPhotosFocalDataCachedCached = cache((focal: number) => - getPhotosFocalDataCached({ + getPhotosFocalLengthDataCached({ focal, limit: INFINITE_SCROLL_GRID_PHOTO_INITIAL, })); diff --git a/src/app/focal/[focal]/share/page.tsx b/src/app/focal/[focal]/share/page.tsx new file mode 100644 index 00000000..01d7aead --- /dev/null +++ b/src/app/focal/[focal]/share/page.tsx @@ -0,0 +1,70 @@ +import { generateMetaForFocalLength, getFocalLengthFromString } from '@/focal'; +import FocalLengthOverview from '@/focal/FocalLengthOverview'; +import FocalLengthShareModal from '@/focal/FocalLengthShareModal'; +import { getPhotosFocalLengthDataCached } from '@/focal/data'; +import { INFINITE_SCROLL_GRID_PHOTO_INITIAL } from '@/photo'; +import type { Metadata } from 'next'; +import { cache } from 'react'; + +const getPhotosFocalLengthDataCachedCached = cache((focal: number) => + getPhotosFocalLengthDataCached({ + focal, + limit: INFINITE_SCROLL_GRID_PHOTO_INITIAL, + })); + +interface FocalLengthProps { + params: { focal: string } +} + +export async function generateMetadata({ + params: { focal: focalString }, +}: FocalLengthProps): Promise { + const focal = getFocalLengthFromString(focalString); + + const [ + photos, + { count, dateRange }, + ] = await getPhotosFocalLengthDataCachedCached(focal); + + const { + url, + title, + description, + images, + } = generateMetaForFocalLength(focal, photos, count, dateRange); + + return { + title, + openGraph: { + title, + description, + images, + url, + }, + twitter: { + images, + description, + card: 'summary_large_image', + }, + description, + }; +} + +export default async function Share({ + params: { focal: focalString }, +}: FocalLengthProps) { + const focal = getFocalLengthFromString(focalString); + + const [ + photos, + { count, dateRange }, + ] = await getPhotosFocalLengthDataCachedCached(focal); + + return <> + + + ; +} diff --git a/src/focal/FocalLengthHeader.tsx b/src/focal/FocalLengthHeader.tsx index 9babb486..b40f459a 100644 --- a/src/focal/FocalLengthHeader.tsx +++ b/src/focal/FocalLengthHeader.tsx @@ -1,6 +1,6 @@ import { Photo, PhotoDateRange } from '@/photo'; import { descriptionForFocalLengthPhotos } from '.'; -import { pathForFocalLength } from '@/site/paths'; +import { pathForFocalLengthShare } from '@/site/paths'; import PhotoSetHeader from '@/photo/PhotoSetHeader'; import PhotoFocalLength from './PhotoFocalLength'; @@ -22,7 +22,6 @@ export default function FocalLengthHeader({ return ( } - entityVerb="Tagged" entityDescription={descriptionForFocalLengthPhotos( photos, undefined, @@ -30,7 +29,7 @@ export default function FocalLengthHeader({ )} photos={photos} selectedPhoto={selectedPhoto} - sharePath={pathForFocalLength(focal)} + sharePath={pathForFocalLengthShare(focal)} indexNumber={indexNumber} count={count} dateRange={dateRange} diff --git a/src/focal/FocalLengthOGTile.tsx b/src/focal/FocalLengthOGTile.tsx new file mode 100644 index 00000000..29e9af83 --- /dev/null +++ b/src/focal/FocalLengthOGTile.tsx @@ -0,0 +1,50 @@ +import { Photo, PhotoDateRange } from '@/photo'; +import { + absolutePathForFocalLengthImage, + pathForFocalLength, +} from '@/site/paths'; +import OGTile from '@/components/OGTile'; +import { descriptionForFocalLengthPhotos, titleForFocalLength } from '.'; + +export type OGLoadingState = 'unloaded' | 'loading' | 'loaded' | 'failed'; + +export default function FocalLengthOGTile({ + focal, + photos, + loadingState: loadingStateExternal, + riseOnHover, + onLoad, + onFail, + retryTime, + count, + dateRange, +}: { + focal: number + photos: Photo[] + loadingState?: OGLoadingState + onLoad?: () => void + onFail?: () => void + riseOnHover?: boolean + retryTime?: number + count?: number + dateRange?: PhotoDateRange +}) { + return ( + + ); +}; diff --git a/src/focal/FocalLengthShareModal.tsx b/src/focal/FocalLengthShareModal.tsx new file mode 100644 index 00000000..62b02b41 --- /dev/null +++ b/src/focal/FocalLengthShareModal.tsx @@ -0,0 +1,26 @@ +import { absolutePathForFocalLength, pathForFocalLength } from '@/site/paths'; +import { Photo, PhotoDateRange } from '../photo'; +import ShareModal from '@/components/ShareModal'; +import FocalLengthOGTile from './FocalLengthOGTile'; + +export default function FocalLengthShareModal({ + focal, + photos, + count, + dateRange, +}: { + focal: number + photos: Photo[] + count?: number + dateRange?: PhotoDateRange +}) { + return ( + + + + ); +}; diff --git a/src/focal/data.ts b/src/focal/data.ts index be5b539d..3001d7fb 100644 --- a/src/focal/data.ts +++ b/src/focal/data.ts @@ -3,7 +3,7 @@ import { getPhotosMetaCached, } from '@/photo/cache'; -export const getPhotosFocalDataCached = ({ +export const getPhotosFocalLengthDataCached = ({ focal, limit, }: { diff --git a/src/focal/index.ts b/src/focal/index.ts index 9104df94..f3f40bd3 100644 --- a/src/focal/index.ts +++ b/src/focal/index.ts @@ -23,7 +23,7 @@ export const titleForFocalLength = ( photos: Photo[], explicitCount?: number, ) => [ - formatFocalLength(focal), + `${formatFocalLength(focal)} Focal Length`, photoQuantityText(explicitCount ?? photos.length), ].join(' '); diff --git a/src/photo/PhotoDetailPage.tsx b/src/photo/PhotoDetailPage.tsx index 6f95b22a..c96b8bf2 100644 --- a/src/photo/PhotoDetailPage.tsx +++ b/src/photo/PhotoDetailPage.tsx @@ -12,6 +12,7 @@ import { FilmSimulation } from '@/simulation'; import FilmSimulationHeader from '@/simulation/FilmSimulationHeader'; import { TAG_HIDDEN } from '@/tag'; import HiddenHeader from '@/tag/HiddenHeader'; +import FocalLengthHeader from '@/focal/FocalLengthHeader'; export default function PhotoDetailPage({ photo, @@ -20,6 +21,7 @@ export default function PhotoDetailPage({ tag, camera, simulation, + focal, indexNumber, count, dateRange, @@ -30,6 +32,7 @@ export default function PhotoDetailPage({ tag?: string camera?: Camera simulation?: FilmSimulation + focal?: number indexNumber?: number count?: number dateRange?: PhotoDateRange @@ -82,6 +85,19 @@ export default function PhotoDetailPage({ dateRange={dateRange} />} />} + {focal && + } + />} } contentSide={ , ]} diff --git a/src/photo/PhotoLinks.tsx b/src/photo/PhotoLinks.tsx index 6a7a5c1b..b2d86036 100644 --- a/src/photo/PhotoLinks.tsx +++ b/src/photo/PhotoLinks.tsx @@ -101,6 +101,7 @@ export default function PhotoLinks({ tag={tag} camera={camera} simulation={simulation} + focal={focal} scroll={false} prefetch > @@ -112,6 +113,7 @@ export default function PhotoLinks({ tag={tag} camera={camera} simulation={simulation} + focal={focal} scroll={false} prefetch > diff --git a/src/site/paths.ts b/src/site/paths.ts index a04b4335..94a30d3a 100644 --- a/src/site/paths.ts +++ b/src/site/paths.ts @@ -125,11 +125,14 @@ export const pathForCameraShare = (camera: Camera) => export const pathForFilmSimulation = (simulation: FilmSimulation) => `${PREFIX_FILM_SIMULATION}/${simulation}`; +export const pathForFilmSimulationShare = (simulation: FilmSimulation) => + `${pathForFilmSimulation(simulation)}/${SHARE}`; + export const pathForFocalLength = (focal: number) => `${PREFIX_FOCAL_LENGTH}/${focal}mm`; -export const pathForFilmSimulationShare = (simulation: FilmSimulation) => - `${pathForFilmSimulation(simulation)}/${SHARE}`; +export const pathForFocalLengthShare = (focal: number) => + `${pathForFocalLength(focal)}/${SHARE}`;; export const absolutePathForPhoto = (params: PhotoPathParams) => `${BASE_URL}${pathForPhoto(params)}`; From 76dc450e9f6d6e4e79feca984dac55e56f76ef5c Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 21 May 2024 12:37:42 -0500 Subject: [PATCH 5/6] Add focal length links to large photos --- src/photo/PhotoLarge.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index e7e7c561..b0331219 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -11,7 +11,11 @@ import SiteGrid from '@/components/SiteGrid'; import ImageLarge from '@/components/image/ImageLarge'; import { clsx } from 'clsx/lite'; import Link from 'next/link'; -import { pathForPhoto, pathForPhotoShare } from '@/site/paths'; +import { + pathForFocalLength, + pathForPhoto, + pathForPhotoShare, +} from '@/site/paths'; import PhotoTags from '@/tag/PhotoTags'; import ShareButton from '@/components/ShareButton'; import PhotoCamera from '../camera/PhotoCamera'; @@ -155,7 +159,10 @@ export default function PhotoLarge({ <>
  • - {photo.focalLengthFormatted} + {photo.focalLength && + + {photo.focalLengthFormatted} + } {photo.focalLengthIn35MmFormatFormatted && <> {' '} From 8b7fe6187fbebeabb44c723325ba62d4a636fd44 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 21 May 2024 17:45:41 -0400 Subject: [PATCH 6/6] Rename test path variables --- __tests__/path.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/__tests__/path.test.ts b/__tests__/path.test.ts index 01cfa03f..5bbe5016 100644 --- a/__tests__/path.test.ts +++ b/__tests__/path.test.ts @@ -30,8 +30,8 @@ const CAMERA_MAKE = 'fujifilm'; const CAMERA_MODEL = 'x-t1'; const CAMERA_OBJECT = { make: CAMERA_MAKE, model: CAMERA_MODEL }; const FILM_SIMULATION = 'acros'; -const FOCAL_LENGTH_NUMBER = 90; -const FOCAL_LENGTH = `${FOCAL_LENGTH_NUMBER}mm`; +const FOCAL_LENGTH = 90; +const FOCAL_LENGTH_STRING = `${FOCAL_LENGTH}mm`; const SHARE = 'share'; const PATH_ROOT = '/'; @@ -61,7 +61,7 @@ const PATH_FILM_SIMULATION_SHARE = `${PATH_FILM_SIMULATION}/${SHARE}`; const PATH_FILM_SIMULATION_PHOTO = `${PATH_FILM_SIMULATION}/${PHOTO_ID}`; const PATH_FILM_SIMULATION_PHOTO_SHARE = `${PATH_FILM_SIMULATION_PHOTO}/${SHARE}`; -const PATH_FOCAL_LENGTH = `/focal/${FOCAL_LENGTH}`; +const PATH_FOCAL_LENGTH = `/focal/${FOCAL_LENGTH_STRING}`; const PATH_FOCAL_LENGTH_SHARE = `${PATH_FOCAL_LENGTH}/${SHARE}`; const PATH_FOCAL_LENGTH_PHOTO = `${PATH_FOCAL_LENGTH}/${PHOTO_ID}`; const PATH_FOCAL_LENGTH_PHOTO_SHARE = `${PATH_FOCAL_LENGTH_PHOTO}/${SHARE}`; @@ -178,18 +178,18 @@ describe('Paths', () => { }); // Focal Length expect(getPathComponents(PATH_FOCAL_LENGTH)).toEqual({ - focal: FOCAL_LENGTH_NUMBER, + focal: FOCAL_LENGTH, }); expect(getPathComponents(PATH_FOCAL_LENGTH_SHARE)).toEqual({ - focal: FOCAL_LENGTH_NUMBER, + focal: FOCAL_LENGTH, }); expect(getPathComponents(PATH_FOCAL_LENGTH_PHOTO)).toEqual({ photoId: PHOTO_ID, - focal: FOCAL_LENGTH_NUMBER, + focal: FOCAL_LENGTH, }); expect(getPathComponents(PATH_FOCAL_LENGTH_PHOTO_SHARE)).toEqual({ photoId: PHOTO_ID, - focal: FOCAL_LENGTH_NUMBER, + focal: FOCAL_LENGTH, }); }); it('can be escaped', () => {