Refactor camera paths and string parsing

This commit is contained in:
Sam Becker 2024-03-29 11:08:51 -05:00
parent ef0b652c97
commit eb94f4f0fb
12 changed files with 119 additions and 100 deletions

View File

@ -1,14 +1,30 @@
import { Camera, formatCameraText } from '@/camera'; import { Camera, formatCameraText } from '@/camera';
const APPLE : Camera = { make: 'Apple', model: 'iPhone 11 Pro' };
const APPLE_01 : Camera = { make: 'Apple', model: 'iPhone 11' };
const APPLE_02 : Camera = { make: 'Apple', model: 'iPhone 15 Pro Max' };
const FUJIFILM : Camera = { make: 'Fujifilm', model: 'X-T5' };
const CANON : Camera = { make: 'Canon', model: 'Canon EOS 800D' };
const NIKON : Camera = {
make: 'Nikon Corporation',
model: 'Nikon D7000',
};
describe('Camera', () => { describe('Camera', () => {
it('labels correctly', () => { it('labels full text correctly', () => {
const apple: Camera = { make: 'Apple', model: 'iPhone 11 Pro' }; expect(formatCameraText(APPLE)).toBe('iPhone 11 Pro');
expect(formatCameraText(apple, true)).toBe('Apple iPhone 11 Pro'); expect(formatCameraText(APPLE, 'always')).toBe('Apple iPhone 11 Pro');
expect(formatCameraText(apple, false)).toBe('iPhone 11 Pro'); expect(formatCameraText(APPLE, 'if-not-apple')).toBe('iPhone 11 Pro');
const fujifilm: Camera = { make: 'Fujifilm', model: 'X-T5' }; expect(formatCameraText(APPLE, 'never')).toBe('iPhone 11 Pro');
expect(formatCameraText(fujifilm)).toBe('Fujifilm X-T5'); expect(formatCameraText(FUJIFILM)).toBe('Fujifilm X-T5');
const canon: Camera = { make: 'Canon', model: 'Canon EOS 800D' }; expect(formatCameraText(CANON)).toBe('Canon EOS 800D');
expect(formatCameraText(canon)).toBe('Canon EOS 800D'); expect(formatCameraText(NIKON)).toBe('Nikon D7000');
});
it('labels models correctly', () => {
expect(formatCameraText(APPLE, 'never')).toBe('iPhone 11 Pro');
expect(formatCameraText(APPLE, 'never', true)).toBe('11 Pro');
expect(formatCameraText(APPLE_01, 'never', true)).toBe('iPhone 11');
expect(formatCameraText(APPLE_02, 'never', true)).toBe('15 Pro Max');
}); });
}); });

View File

@ -18,12 +18,12 @@ import {
isPathTagPhotoShare, isPathTagPhotoShare,
isPathTagShare, isPathTagShare,
} from '@/site/paths'; } from '@/site/paths';
import { getCameraFromKey } from '@/camera';
const PHOTO_ID = 'UsKSGcbt'; const PHOTO_ID = 'UsKSGcbt';
const TAG = 'tag-name'; const TAG = 'tag-name';
const CAMERA = 'fujifilm-x-t1'; const CAMERA_MAKE = 'fujifilm';
const CAMERA_OBJECT = getCameraFromKey(CAMERA); const CAMERA_MODEL = 'x-t1';
const CAMERA_OBJECT = { make: CAMERA_MAKE, model: CAMERA_MODEL };
const FILM_SIMULATION = 'acros'; const FILM_SIMULATION = 'acros';
const SHARE = 'share'; const SHARE = 'share';
@ -39,7 +39,7 @@ const PATH_TAG_SHARE = `${PATH_TAG}/${SHARE}`;
const PATH_TAG_PHOTO = `${PATH_TAG}/${PHOTO_ID}`; const PATH_TAG_PHOTO = `${PATH_TAG}/${PHOTO_ID}`;
const PATH_TAG_PHOTO_SHARE = `${PATH_TAG_PHOTO}/${SHARE}`; const PATH_TAG_PHOTO_SHARE = `${PATH_TAG_PHOTO}/${SHARE}`;
const PATH_CAMERA = `/shot-on/${CAMERA}`; const PATH_CAMERA = `/shot-on/${CAMERA_MAKE}/${CAMERA_MODEL}`;
const PATH_CAMERA_SHARE = `${PATH_CAMERA}/${SHARE}`; const PATH_CAMERA_SHARE = `${PATH_CAMERA}/${SHARE}`;
const PATH_CAMERA_PHOTO = `${PATH_CAMERA}/${PHOTO_ID}`; const PATH_CAMERA_PHOTO = `${PATH_CAMERA}/${PHOTO_ID}`;
const PATH_CAMERA_PHOTO_SHARE = `${PATH_CAMERA_PHOTO}/${SHARE}`; const PATH_CAMERA_PHOTO_SHARE = `${PATH_CAMERA_PHOTO}/${SHARE}`;

View File

@ -11,16 +11,12 @@ import {
} from '@/site/paths'; } from '@/site/paths';
import PhotoDetailPage from '@/photo/PhotoDetailPage'; import PhotoDetailPage from '@/photo/PhotoDetailPage';
import { getPhotoCached } from '@/photo/cache'; import { getPhotoCached } from '@/photo/cache';
import { cameraFromPhoto } from '@/camera'; import { PhotoCameraProps, cameraFromPhoto } from '@/camera';
import { getPhotosCameraDataCached } from '@/camera/data'; import { getPhotosCameraDataCached } from '@/camera/data';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
interface PhotoCameraProps {
params: { photoId: string, camera: string }
}
export async function generateMetadata({ export async function generateMetadata({
params: { photoId, camera }, params: { photoId, make, model },
}: PhotoCameraProps): Promise<Metadata> { }: PhotoCameraProps): Promise<Metadata> {
const photo = await getPhotoCached(photoId); const photo = await getPhotoCached(photoId);
@ -32,7 +28,7 @@ export async function generateMetadata({
const url = absolutePathForPhoto( const url = absolutePathForPhoto(
photo, photo,
undefined, undefined,
cameraFromPhoto(photo, camera), cameraFromPhoto(photo, { make, model }),
); );
return { return {
@ -54,14 +50,14 @@ export async function generateMetadata({
} }
export default async function PhotoCameraPage({ export default async function PhotoCameraPage({
params: { photoId, camera: cameraProp }, params: { photoId, make, model },
children, children,
}: PhotoCameraProps & { children: ReactNode }) { }: PhotoCameraProps & { children: ReactNode }) {
const photo = await getPhotoCached(photoId); const photo = await getPhotoCached(photoId);
if (!photo) { redirect(PATH_ROOT); } if (!photo) { redirect(PATH_ROOT); }
const camera = cameraFromPhoto(photo, cameraProp); const camera = cameraFromPhoto(photo, { make, model });
const [ const [
photos, photos,

View File

@ -1,19 +1,17 @@
import { getPhotoCached } from '@/photo/cache'; import { getPhotoCached } from '@/photo/cache';
import { cameraFromPhoto } from '@/camera'; import { PhotoCameraProps, cameraFromPhoto } from '@/camera';
import PhotoShareModal from '@/photo/PhotoShareModal'; import PhotoShareModal from '@/photo/PhotoShareModal';
import { PATH_ROOT } from '@/site/paths'; import { PATH_ROOT } from '@/site/paths';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
export default async function Share({ export default async function Share({
params: { photoId, camera: cameraProp }, params: { photoId, make, model },
}: { }: PhotoCameraProps) {
params: { photoId: string, camera: string }
}) {
const photo = await getPhotoCached(photoId); const photo = await getPhotoCached(photoId);
if (!photo) { return redirect(PATH_ROOT); } if (!photo) { return redirect(PATH_ROOT); }
const camera = cameraFromPhoto(photo, cameraProp); const camera = cameraFromPhoto(photo, { make, model });
return <PhotoShareModal {...{ photo, camera }} />; return <PhotoShareModal {...{ photo, camera }} />;
} }

View File

@ -1,5 +1,5 @@
import { getPhotosCached } from '@/photo/cache'; import { getPhotosCached } from '@/photo/cache';
import { getCameraFromKey } from '@/camera'; import { CameraProps, getCameraFromParams } from '@/camera';
import { import {
IMAGE_OG_DIMENSION_SMALL, IMAGE_OG_DIMENSION_SMALL,
MAX_PHOTOS_TO_SHOW_PER_TAG, MAX_PHOTOS_TO_SHOW_PER_TAG,
@ -13,9 +13,9 @@ export const runtime = 'edge';
export async function GET( export async function GET(
_: Request, _: Request,
context: { params: { camera: string } }, context: CameraProps,
) { ) {
const camera = getCameraFromKey(context.params.camera); const camera = getCameraFromParams(context.params);
const [ const [
photos, photos,

View File

@ -1,4 +1,4 @@
import { getCameraFromKey } from '@/camera'; import { CameraProps, getCameraFromParams } from '@/camera';
import { Metadata } from 'next'; import { Metadata } from 'next';
import { generateMetaForCamera } from '@/camera/meta'; import { generateMetaForCamera } from '@/camera/meta';
import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo'; import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo';
@ -9,14 +9,10 @@ import {
} from '@/camera/data'; } from '@/camera/data';
import CameraOverview from '@/camera/CameraOverview'; import CameraOverview from '@/camera/CameraOverview';
interface CameraProps {
params: { camera: string },
}
export async function generateMetadata({ export async function generateMetadata({
params, params,
}: CameraProps): Promise<Metadata> { }: CameraProps): Promise<Metadata> {
const camera = getCameraFromKey(params.camera); const camera = getCameraFromParams(params);
const [ const [
photos, photos,
@ -55,7 +51,7 @@ export default async function CameraPage({
params, params,
searchParams, searchParams,
}: CameraProps & PaginationParams) { }: CameraProps & PaginationParams) {
const camera = getCameraFromKey(params.camera); const camera = getCameraFromParams(params);
const { const {
photos, photos,

View File

@ -1,4 +1,8 @@
import { cameraFromPhoto, getCameraFromKey } from '@/camera'; import {
CameraProps,
cameraFromPhoto,
getCameraFromParams,
} from '@/camera';
import CameraShareModal from '@/camera/CameraShareModal'; import CameraShareModal from '@/camera/CameraShareModal';
import { generateMetaForCamera } from '@/camera/meta'; import { generateMetaForCamera } from '@/camera/meta';
import { Metadata } from 'next'; import { Metadata } from 'next';
@ -10,14 +14,10 @@ import {
} from '@/camera/data'; } from '@/camera/data';
import CameraOverview from '@/camera/CameraOverview'; import CameraOverview from '@/camera/CameraOverview';
interface CameraProps {
params: { camera: string }
}
export async function generateMetadata({ export async function generateMetadata({
params, params,
}: CameraProps): Promise<Metadata> { }: CameraProps): Promise<Metadata> {
const camera = getCameraFromKey(params.camera); const camera = getCameraFromParams(params);
const [ const [
photos, photos,
@ -56,7 +56,7 @@ export default async function Share({
params, params,
searchParams, searchParams,
}: CameraProps & PaginationParams) { }: CameraProps & PaginationParams) {
const cameraFromParams = getCameraFromKey(params.camera); const cameraFromParams = getCameraFromParams(params);
const { const {
photos, photos,

View File

@ -8,6 +8,14 @@ export type Camera = {
model: string model: string
}; };
export interface CameraProps {
params: Camera
}
export interface PhotoCameraProps {
params: Camera & { photoId: string }
}
export type CameraWithCount = { export type CameraWithCount = {
cameraKey: string cameraKey: string
camera: Camera camera: Camera
@ -19,11 +27,16 @@ export type Cameras = CameraWithCount[];
export const createCameraKey = ({ make, model }: Camera) => export const createCameraKey = ({ make, model }: Camera) =>
parameterize(`${make}-${model}`, true); parameterize(`${make}-${model}`, true);
// Assumes no makes ('Fujifilm,' 'Apple,' 'Canon', etc.) have dashes export const getCameraFromParams = ({
export const getCameraFromKey = (cameraKey: string): Camera => { make,
const [make, model] = cameraKey.toLowerCase().split(/[-| ](.*)/s); model,
return { make, model }; }: {
}; make: string,
model: string,
}): Camera => ({
make: parameterize(make, true),
model: parameterize(model, true),
});
export const sortCamerasWithCount = ( export const sortCamerasWithCount = (
a: CameraWithCount, a: CameraWithCount,
@ -36,36 +49,34 @@ export const sortCamerasWithCount = (
export const cameraFromPhoto = ( export const cameraFromPhoto = (
photo: Photo | undefined, photo: Photo | undefined,
fallback?: Camera | string, fallback?: Camera,
): Camera => ): Camera =>
photo?.make && photo?.model photo?.make && photo?.model
? { make: photo.make, model: photo.model } ? { make: photo.make, model: photo.model }
: typeof fallback === 'string' : fallback ?? CAMERA_PLACEHOLDER;
? getCameraFromKey(fallback)
: fallback ?? CAMERA_PLACEHOLDER;
export const formatCameraText = ( export const formatCameraText = (
{ make, model: modelRaw }: Camera, { make: makeRaw, model: modelRaw }: Camera,
includeMakeApple?: boolean, includeMake: 'always' | 'never' | 'if-not-apple' = 'if-not-apple',
removeIPhoneOnLongModels?: boolean
) => { ) => {
// Remove 'Corporation' from makes like 'Nikon Corporation'
const make = makeRaw.replace(/ Corporation/i, '');
// Remove potential duplicate make from model // Remove potential duplicate make from model
const model = modelRaw.replace(`${make} `, ''); let model = modelRaw.replace(`${make} `, '');
return make === 'Apple' && !includeMakeApple if (
? model removeIPhoneOnLongModels &&
model.includes('iPhone') &&
model.length > 9
) {
model = model.replace(/iPhone\s*/i, '');
}
return (
includeMake === 'never' ||
includeMake === 'if-not-apple' && make === 'Apple'
) ? model
: `${make} ${model}`; : `${make} ${model}`;
}; };
export const formatCameraModelText = ( export const formatCameraModelTextShort = (camera: Camera) =>
{ make, model: modelRaw }: Camera, formatCameraText(camera, 'never', true);
) => {
// Remove potential duplicate make from model
const model = modelRaw.replace(`${make} `, '');
const textLength = model?.length ?? 0;
if (textLength > 0 && textLength <= 8) {
return model;
} else if (model?.includes('iPhone')) {
return model.split('iPhone')[1];
} else {
return undefined;
}
};

View File

@ -5,7 +5,7 @@ import ImagePhotoGrid from './components/ImagePhotoGrid';
import ImageContainer from './components/ImageContainer'; import ImageContainer from './components/ImageContainer';
import { OG_TEXT_BOTTOM_ALIGNMENT } from '@/site/config'; import { OG_TEXT_BOTTOM_ALIGNMENT } from '@/site/config';
import { NextImageSize } from '@/services/next-image'; import { NextImageSize } from '@/services/next-image';
import { cameraFromPhoto, formatCameraModelText } from '@/camera'; import { cameraFromPhoto, formatCameraModelTextShort } from '@/camera';
export default function PhotoImageResponse({ export default function PhotoImageResponse({
photo, photo,
@ -19,7 +19,7 @@ export default function PhotoImageResponse({
fontFamily: string fontFamily: string
}) { }) {
const model = photo.model const model = photo.model
? formatCameraModelText(cameraFromPhoto(photo)) ? formatCameraModelTextShort(cameraFromPhoto(photo))
: undefined; : undefined;
return ( return (

View File

@ -191,7 +191,7 @@ const sqlGetPhotosTagCount = async (tag: string) => sql`
const sqlGetPhotosCameraCount = async (camera: Camera) => sql` const sqlGetPhotosCameraCount = async (camera: Camera) => sql`
SELECT COUNT(*) FROM photos SELECT COUNT(*) FROM photos
WHERE WHERE
LOWER(make)=${parameterize(camera.make, true)} AND LOWER(REPLACE(make, ' ', '-'))=${parameterize(camera.make, true)} AND
LOWER(REPLACE(model, ' ', '-'))=${parameterize(camera.model, true)} AND LOWER(REPLACE(model, ' ', '-'))=${parameterize(camera.model, true)} AND
hidden IS NOT TRUE hidden IS NOT TRUE
`.then(({ rows }) => parseInt(rows[0].count, 10)); `.then(({ rows }) => parseInt(rows[0].count, 10));
@ -225,7 +225,7 @@ const sqlGetPhotosCameraDateRange = async (camera: Camera) => sql`
SELECT MIN(taken_at_naive) as start, MAX(taken_at_naive) as end SELECT MIN(taken_at_naive) as start, MAX(taken_at_naive) as end
FROM photos FROM photos
WHERE WHERE
LOWER(make)=${parameterize(camera.make, true)} AND LOWER(REPLACE(make, ' ', '-'))=${parameterize(camera.make, true)} AND
LOWER(REPLACE(model, ' ', '-'))=${parameterize(camera.model, true)} AND LOWER(REPLACE(model, ' ', '-'))=${parameterize(camera.model, true)} AND
hidden IS NOT TRUE hidden IS NOT TRUE
`.then(({ rows }) => rows[0]?.start && rows[0]?.end `.then(({ rows }) => rows[0]?.start && rows[0]?.end
@ -380,7 +380,7 @@ export const getPhotos = async (options: GetPhotosOptions = {}) => {
values.push(tag); values.push(tag);
} }
if (camera) { if (camera) {
wheres.push(`LOWER(make)=$${valueIndex++}`); wheres.push(`LOWER(REPLACE(make, ' ', '-'))=$${valueIndex++}`);
wheres.push(`LOWER(REPLACE(model, ' ', '-'))=$${valueIndex++}`); wheres.push(`LOWER(REPLACE(model, ' ', '-'))=$${valueIndex++}`);
values.push(parameterize(camera.make, true)); values.push(parameterize(camera.make, true));
values.push(parameterize(camera.model, true)); values.push(parameterize(camera.model, true));

View File

@ -1,11 +1,8 @@
import { Photo } from '@/photo'; import { Photo } from '@/photo';
import { BASE_URL } from './config'; import { BASE_URL } from './config';
import { import { Camera } from '@/camera';
Camera,
createCameraKey,
getCameraFromKey,
} from '@/camera';
import { FilmSimulation } from '@/simulation'; import { FilmSimulation } from '@/simulation';
import { parameterize } from '@/utility/string';
// Core paths // Core paths
export const PATH_ROOT = '/'; export const PATH_ROOT = '/';
@ -24,7 +21,7 @@ export const PREFIX_FILM_SIMULATION = '/film';
// Dynamic paths // Dynamic paths
const PATH_PHOTO_DYNAMIC = `${PREFIX_PHOTO}/[photoId]`; const PATH_PHOTO_DYNAMIC = `${PREFIX_PHOTO}/[photoId]`;
const PATH_TAG_DYNAMIC = `${PREFIX_TAG}/[tag]`; const PATH_TAG_DYNAMIC = `${PREFIX_TAG}/[tag]`;
const PATH_CAMERA_DYNAMIC = `${PREFIX_CAMERA}/[camera]`; const PATH_CAMERA_DYNAMIC = `${PREFIX_CAMERA}/[make]/[model]`;
const PATH_FILM_SIMULATION_DYNAMIC = `${PREFIX_FILM_SIMULATION}/[simulation]`; const PATH_FILM_SIMULATION_DYNAMIC = `${PREFIX_FILM_SIMULATION}/[simulation]`;
// Admin paths // Admin paths
@ -126,8 +123,11 @@ export const pathForTag = (tag: string, next?: number) =>
export const pathForTagShare = (tag: string) => export const pathForTagShare = (tag: string) =>
`${pathForTag(tag)}/${SHARE}`; `${pathForTag(tag)}/${SHARE}`;
export const pathForCamera = (camera: Camera, next?: number) => export const pathForCamera = ({ make, model }: Camera, next?: number) =>
pathWithNext(`${PREFIX_CAMERA}/${createCameraKey(camera)}`, next); pathWithNext(
`${PREFIX_CAMERA}/${parameterize(make, true)}/${parameterize(model, true)}`,
next,
);
export const pathForCameraShare = (camera: Camera) => export const pathForCameraShare = (camera: Camera) =>
`${pathForCamera(camera)}/${SHARE}`; `${pathForCamera(camera)}/${SHARE}`;
@ -196,22 +196,22 @@ export const isPathTagPhoto = (pathname = '') =>
export const isPathTagPhotoShare = (pathname = '') => export const isPathTagPhotoShare = (pathname = '') =>
new RegExp(`^${PREFIX_TAG}/[^/]+/[^/]+/${SHARE}/?$`).test(pathname); new RegExp(`^${PREFIX_TAG}/[^/]+/[^/]+/${SHARE}/?$`).test(pathname);
// shot-on/[camera] // shot-on/[make]/[model]
export const isPathCamera = (pathname = '') => export const isPathCamera = (pathname = '') =>
new RegExp(`^${PREFIX_CAMERA}/[^/]+/?$`).test(pathname);
// shot-on/[camera]/share
export const isPathCameraShare = (pathname = '') =>
new RegExp(`^${PREFIX_CAMERA}/[^/]+/${SHARE}/?$`).test(pathname);
// shot-on/[camera]/[photoId]
export const isPathCameraPhoto = (pathname = '') =>
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/?$`).test(pathname); new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/?$`).test(pathname);
// shot-on/[camera]/[photoId]/share // shot-on/[make]/[model]/share
export const isPathCameraPhotoShare = (pathname = '') => export const isPathCameraShare = (pathname = '') =>
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/${SHARE}/?$`).test(pathname); new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/${SHARE}/?$`).test(pathname);
// shot-on/[make]/[model]/[photoId]
export const isPathCameraPhoto = (pathname = '') =>
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/[^/]+/?$`).test(pathname);
// shot-on/[make]/[model]/[photoId]/share
export const isPathCameraPhotoShare = (pathname = '') =>
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/[^/]+/${SHARE}/?$`).test(pathname);
// film/[simulation] // film/[simulation]
export const isPathFilmSimulation = (pathname = '') => export const isPathFilmSimulation = (pathname = '') =>
new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/?$`).test(pathname); new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/?$`).test(pathname);
@ -258,18 +258,20 @@ export const getPathComponents = (pathname = ''): {
const photoIdFromTag = pathname.match( const photoIdFromTag = pathname.match(
new RegExp(`^${PREFIX_TAG}/[^/]+/((?!${SHARE})[^/]+)`))?.[1]; new RegExp(`^${PREFIX_TAG}/[^/]+/((?!${SHARE})[^/]+)`))?.[1];
const photoIdFromCamera = pathname.match( const photoIdFromCamera = pathname.match(
new RegExp(`^${PREFIX_CAMERA}/[^/]+/((?!${SHARE})[^/]+)`))?.[1]; new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/((?!${SHARE})[^/]+)`))?.[1];
const photoIdFromFilmSimulation = pathname.match( const photoIdFromFilmSimulation = pathname.match(
new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/((?!${SHARE})[^/]+)`))?.[1]; new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/((?!${SHARE})[^/]+)`))?.[1];
const tag = pathname.match( const tag = pathname.match(
new RegExp(`^${PREFIX_TAG}/([^/]+)`))?.[1]; new RegExp(`^${PREFIX_TAG}/([^/]+)`))?.[1];
const cameraString = pathname.match( const cameraMake = pathname.match(
new RegExp(`^${PREFIX_CAMERA}/([^/]+)`))?.[1]; new RegExp(`^${PREFIX_CAMERA}/([^/]+)`))?.[1];
const cameraModel = pathname.match(
new RegExp(`^${PREFIX_CAMERA}/[^/]+/([^/]+)`))?.[1];
const simulation = pathname.match( const simulation = pathname.match(
new RegExp(`^${PREFIX_FILM_SIMULATION}/([^/]+)`))?.[1] as FilmSimulation; new RegExp(`^${PREFIX_FILM_SIMULATION}/([^/]+)`))?.[1] as FilmSimulation;
const camera = cameraString const camera = cameraMake && cameraModel
? getCameraFromKey(cameraString) ? { make: cameraMake, model: cameraModel }
: undefined; : undefined;
return { return {