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