diff --git a/__tests__/path.test.ts b/__tests__/path.test.ts
index 0552a40f..5bbe5016 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 = 90;
+const FOCAL_LENGTH_STRING = `${FOCAL_LENGTH}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_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}`;
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,
+ });
+ expect(getPathComponents(PATH_FOCAL_LENGTH_SHARE)).toEqual({
+ focal: FOCAL_LENGTH,
+ });
+ expect(getPathComponents(PATH_FOCAL_LENGTH_PHOTO)).toEqual({
+ photoId: PHOTO_ID,
+ focal: FOCAL_LENGTH,
+ });
+ expect(getPathComponents(PATH_FOCAL_LENGTH_PHOTO_SHARE)).toEqual({
+ photoId: PHOTO_ID,
+ focal: FOCAL_LENGTH,
+ });
});
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/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]/[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]/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..82948194
--- /dev/null
+++ b/src/app/focal/[focal]/page.tsx
@@ -0,0 +1,71 @@
+import { generateMetaForFocalLength, getFocalLengthFromString } from '@/focal';
+import FocalLengthOverview from '@/focal/FocalLengthOverview';
+import { getPhotosFocalLengthDataCached } 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) =>
+ 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 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/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/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..b40f459a
--- /dev/null
+++ b/src/focal/FocalLengthHeader.tsx
@@ -0,0 +1,38 @@
+import { Photo, PhotoDateRange } from '@/photo';
+import { descriptionForFocalLengthPhotos } from '.';
+import { pathForFocalLengthShare } 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 (
+ }
+ entityDescription={descriptionForFocalLengthPhotos(
+ photos,
+ undefined,
+ count,
+ )}
+ photos={photos}
+ selectedPhoto={selectedPhoto}
+ 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/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/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/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..3001d7fb
--- /dev/null
+++ b/src/focal/data.ts
@@ -0,0 +1,17 @@
+import {
+ getPhotosCached,
+ getPhotosMetaCached,
+} from '@/photo/cache';
+
+export const getPhotosFocalLengthDataCached = ({
+ 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
new file mode 100644
index 00000000..f3f40bd3
--- /dev/null
+++ b/src/focal/index.ts
@@ -0,0 +1,59 @@
+import {
+ Photo,
+ PhotoDateRange,
+ descriptionForPhotoSet,
+ photoQuantityText,
+} from '@/photo';
+import {
+ absolutePathForFocalLength,
+ 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,
+) => [
+ `${formatFocalLength(focal)} Focal Length`,
+ 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/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/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/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..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';
@@ -40,6 +44,7 @@ export default function PhotoLarge({
shouldShareTag,
shouldShareCamera,
shouldShareSimulation,
+ shouldShareFocalLength,
shouldScrollOnShare,
onVisible,
}: {
@@ -54,6 +59,7 @@ export default function PhotoLarge({
shouldShareTag?: boolean
shouldShareCamera?: boolean
shouldShareSimulation?: boolean
+ shouldShareFocalLength?: boolean
shouldScrollOnShare?: boolean
onVisible?: () => void
}) {
@@ -76,7 +82,7 @@ export default function PhotoLarge({
containerRef={ref}
contentMain={
-
- {photo.focalLengthFormatted}
+ {photo.focalLength &&
+
+ {photo.focalLengthFormatted}
+ }
{photo.focalLengthIn35MmFormatFormatted &&
<>
{' '}
@@ -189,12 +198,14 @@ export default function PhotoLarge({
'md:translate-x-[-2.5px]',
'translate-y-[1.5px] md:translate-y-0',
)}
- path={pathForPhotoShare(
+ path={pathForPhotoShare({
photo,
- shouldShareTag ? primaryTag : undefined,
- shouldShareCamera ? camera : undefined,
- shouldShareSimulation ? photo.filmSimulation : undefined,
- )}
+ tag: shouldShareTag ? primaryTag : undefined,
+ camera: shouldShareCamera ? camera : undefined,
+ // eslint-disable-next-line max-len
+ simulation: shouldShareSimulation ? photo.filmSimulation : undefined,
+ focal: shouldShareFocalLength ? photo.focalLength : undefined,
+ })}
prefetch={prefetchRelatedLinks}
shouldScroll={shouldScrollOnShare}
/>
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..b2d86036 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 (
@@ -86,6 +101,7 @@ export default function PhotoLinks({
tag={tag}
camera={camera}
simulation={simulation}
+ focal={focal}
scroll={false}
prefetch
>
@@ -97,6 +113,7 @@ export default function PhotoLinks({
tag={tag}
camera={camera}
simulation={simulation}
+ focal={focal}
scroll={false}
prefetch
>
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/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/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/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
diff --git a/src/site/paths.ts b/src/site/paths.ts
index 9996ac6f..94a30d3a 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,21 @@ export const PATHS_TO_CACHE = [
PATH_TAG_DYNAMIC,
PATH_CAMERA_DYNAMIC,
PATH_FILM_SIMULATION_DYNAMIC,
+ PATH_FOCAL_LENGTH_DYNAMIC,
...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`;
-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,20 +83,18 @@ 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) =>
typeof photoOrPhotoId === 'string' ? photoOrPhotoId : photoOrPhotoId.id;
-export const pathForPhoto = (
- photo: PhotoOrPhotoId,
- tag?: string,
- camera?: Camera,
- simulation?: FilmSimulation,
-) =>
+export const pathForPhoto = ({
+ photo,
+ tag,
+ camera,
+ simulation,
+ focal,
+}: PhotoPathParams) =>
typeof photo !== 'string' && photo.hidden
? `${pathForTag(TAG_HIDDEN)}/${getPhotoId(photo)}`
: tag
@@ -107,51 +103,39 @@ 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,
- 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, 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 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 pathForFocalLength = (focal: number) =>
+ `${PREFIX_FOCAL_LENGTH}/${focal}mm`;
+
+export const pathForFocalLengthShare = (focal: number) =>
+ `${pathForFocalLength(focal)}/${SHARE}`;;
+
+export const absolutePathForPhoto = (params: PhotoPathParams) =>
+ `${BASE_URL}${pathForPhoto(params)}`;
export const absolutePathForTag = (tag: string) =>
`${BASE_URL}${pathForTag(tag)}`;
@@ -162,8 +146,11 @@ 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`;
+ `${absolutePathForPhoto({ photo })}/image`;
export const absolutePathForTagImage = (tag: string) =>
`${absolutePathForTag(tag)}/image`;
@@ -175,6 +162,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 +223,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 +268,7 @@ export const getPathComponents = (pathname = ''): {
tag?: string
camera?: Camera
simulation?: FilmSimulation
+ focal?: number
} => {
const photoIdFromPhoto = pathname.match(
new RegExp(`^${PREFIX_PHOTO}/([^/]+)`))?.[1];
@@ -269,6 +278,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,41 +288,57 @@ 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)) {
- 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({ photo: photoId, focal });
} else if (photoId && isPathPhotoShare(pathname)) {
- return pathForPhoto(photoId);
+ return pathForPhoto({ photo: photoId });
} else if (tag && (
isPathTagPhoto(pathname) ||
isPathTagShare(pathname)
@@ -327,5 +354,10 @@ export const getEscapePath = (pathname?: string) => {
isPathFilmSimulationShare(pathname)
)) {
return pathForFilmSimulation(simulation);
+ } else if (focal && (
+ isPathFocalLengthPhoto(pathname) ||
+ isPathFocalLengthShare(pathname)
+ )) {
+ return pathForFocalLength(focal);
}
};
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;