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';
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', () => {
it('labels correctly', () => {
const apple: Camera = { make: 'Apple', model: 'iPhone 11 Pro' };
expect(formatCameraText(apple, true)).toBe('Apple iPhone 11 Pro');
expect(formatCameraText(apple, false)).toBe('iPhone 11 Pro');
const fujifilm: Camera = { make: 'Fujifilm', model: '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');
it('labels full text correctly', () => {
expect(formatCameraText(APPLE)).toBe('iPhone 11 Pro');
expect(formatCameraText(APPLE, 'always')).toBe('Apple iPhone 11 Pro');
expect(formatCameraText(APPLE, 'if-not-apple')).toBe('iPhone 11 Pro');
expect(formatCameraText(APPLE, 'never')).toBe('iPhone 11 Pro');
expect(formatCameraText(FUJIFILM)).toBe('Fujifilm X-T5');
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,
isPathTagShare,
} from '@/site/paths';
import { getCameraFromKey } from '@/camera';
const PHOTO_ID = 'UsKSGcbt';
const TAG = 'tag-name';
const CAMERA = 'fujifilm-x-t1';
const CAMERA_OBJECT = getCameraFromKey(CAMERA);
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';
@ -39,7 +39,7 @@ const PATH_TAG_SHARE = `${PATH_TAG}/${SHARE}`;
const PATH_TAG_PHOTO = `${PATH_TAG}/${PHOTO_ID}`;
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_PHOTO = `${PATH_CAMERA}/${PHOTO_ID}`;
const PATH_CAMERA_PHOTO_SHARE = `${PATH_CAMERA_PHOTO}/${SHARE}`;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,14 @@ export type Camera = {
model: string
};
export interface CameraProps {
params: Camera
}
export interface PhotoCameraProps {
params: Camera & { photoId: string }
}
export type CameraWithCount = {
cameraKey: string
camera: Camera
@ -19,11 +27,16 @@ export type Cameras = CameraWithCount[];
export const createCameraKey = ({ make, model }: Camera) =>
parameterize(`${make}-${model}`, true);
// Assumes no makes ('Fujifilm,' 'Apple,' 'Canon', etc.) have dashes
export const getCameraFromKey = (cameraKey: string): Camera => {
const [make, model] = cameraKey.toLowerCase().split(/[-| ](.*)/s);
return { make, model };
};
export const getCameraFromParams = ({
make,
model,
}: {
make: string,
model: string,
}): Camera => ({
make: parameterize(make, true),
model: parameterize(model, true),
});
export const sortCamerasWithCount = (
a: CameraWithCount,
@ -36,36 +49,34 @@ export const sortCamerasWithCount = (
export const cameraFromPhoto = (
photo: Photo | undefined,
fallback?: Camera | string,
fallback?: Camera,
): Camera =>
photo?.make && photo?.model
? { make: photo.make, model: photo.model }
: typeof fallback === 'string'
? getCameraFromKey(fallback)
: fallback ?? CAMERA_PLACEHOLDER;
: fallback ?? CAMERA_PLACEHOLDER;
export const formatCameraText = (
{ make, model: modelRaw }: Camera,
includeMakeApple?: boolean,
{ make: makeRaw, model: modelRaw }: Camera,
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
const model = modelRaw.replace(`${make} `, '');
return make === 'Apple' && !includeMakeApple
? model
let model = modelRaw.replace(`${make} `, '');
if (
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}`;
};
export const formatCameraModelText = (
{ make, model: modelRaw }: Camera,
) => {
// 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;
}
};
export const formatCameraModelTextShort = (camera: Camera) =>
formatCameraText(camera, 'never', true);

View File

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

View File

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

View File

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