Refactor camera paths and string parsing
This commit is contained in:
parent
ef0b652c97
commit
eb94f4f0fb
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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}`;
|
||||
|
||||
@ -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,
|
||||
@ -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 }} />;
|
||||
}
|
||||
@ -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,
|
||||
@ -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,
|
||||
@ -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,
|
||||
@ -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);
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user