Rename device to camera

This commit is contained in:
Sam Becker 2023-10-03 14:29:33 -05:00
parent 886ff9224d
commit 69b61d1244
34 changed files with 504 additions and 497 deletions

View File

@ -2,10 +2,10 @@ import '@testing-library/jest-dom';
import {
getEscapePath,
getPathComponents,
isPathDevice,
isPathDevicePhoto,
isPathDevicePhotoShare,
isPathDeviceShare,
isPathCamera,
isPathCameraPhoto,
isPathCameraPhotoShare,
isPathCameraShare,
isPathPhoto,
isPathPhotoShare,
isPathTag,
@ -13,12 +13,12 @@ import {
isPathTagPhotoShare,
isPathTagShare,
} from '@/site/paths';
import { getMakeModelFromDeviceString } from '@/device';
import { getMakeModelFromCameraString } from '@/camera';
const PHOTO_ID = 'UsKSGcbt';
const TAG = 'tag-name';
const DEVICE = 'fujifilm-x-t1';
const DEVICE_OBJECT = getMakeModelFromDeviceString(DEVICE);
const CAMERA = 'fujifilm-x-t1';
const CAMERA_OBJECT = getMakeModelFromCameraString(CAMERA);
const SHARE = 'share';
const PATH_ROOT = '/';
@ -33,10 +33,10 @@ 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_DEVICE = `/shot-on/${DEVICE}`;
const PATH_DEVICE_SHARE = `${PATH_DEVICE}/${SHARE}`;
const PATH_DEVICE_PHOTO = `${PATH_DEVICE}/${PHOTO_ID}`;
const PATH_DEVICE_PHOTO_SHARE = `${PATH_DEVICE_PHOTO}/${SHARE}`;
const PATH_CAMERA = `/shot-on/${CAMERA}`;
const PATH_CAMERA_SHARE = `${PATH_CAMERA}/${SHARE}`;
const PATH_CAMERA_PHOTO = `${PATH_CAMERA}/${PHOTO_ID}`;
const PATH_CAMERA_PHOTO_SHARE = `${PATH_CAMERA_PHOTO}/${SHARE}`;
describe('Paths', () => {
it('can be classified', () => {
@ -47,10 +47,10 @@ describe('Paths', () => {
expect(isPathTagShare(PATH_TAG_SHARE)).toBe(true);
expect(isPathTagPhoto(PATH_TAG_PHOTO)).toBe(true);
expect(isPathTagPhotoShare(PATH_TAG_PHOTO_SHARE)).toBe(true);
expect(isPathDevice(PATH_DEVICE)).toBe(true);
expect(isPathDeviceShare(PATH_DEVICE_SHARE)).toBe(true);
expect(isPathDevicePhoto(PATH_DEVICE_PHOTO)).toBe(true);
expect(isPathDevicePhotoShare(PATH_DEVICE_PHOTO_SHARE)).toBe(true);
expect(isPathCamera(PATH_CAMERA)).toBe(true);
expect(isPathCameraShare(PATH_CAMERA_SHARE)).toBe(true);
expect(isPathCameraPhoto(PATH_CAMERA_PHOTO)).toBe(true);
expect(isPathCameraPhotoShare(PATH_CAMERA_PHOTO_SHARE)).toBe(true);
// Negative
expect(isPathPhoto(PATH_TAG_PHOTO_SHARE)).toBe(false);
expect(isPathPhotoShare(PATH_TAG_PHOTO)).toBe(false);
@ -58,10 +58,10 @@ describe('Paths', () => {
expect(isPathTagShare(PATH_TAG)).toBe(false);
expect(isPathTagPhoto(PATH_PHOTO_SHARE)).toBe(false);
expect(isPathTagPhotoShare(PATH_PHOTO)).toBe(false);
expect(isPathDevice(PATH_TAG_SHARE)).toBe(false);
expect(isPathDeviceShare(PATH_TAG)).toBe(false);
expect(isPathDevicePhoto(PATH_PHOTO_SHARE)).toBe(false);
expect(isPathDevicePhotoShare(PATH_PHOTO)).toBe(false);
expect(isPathCamera(PATH_TAG_SHARE)).toBe(false);
expect(isPathCameraShare(PATH_TAG)).toBe(false);
expect(isPathCameraPhoto(PATH_PHOTO_SHARE)).toBe(false);
expect(isPathCameraPhotoShare(PATH_PHOTO)).toBe(false);
});
it('can be parsed', () => {
expect(getPathComponents(PATH_ROOT)).toEqual({});
@ -85,19 +85,19 @@ describe('Paths', () => {
photoId: PHOTO_ID,
tag: TAG,
});
expect(getPathComponents(PATH_DEVICE)).toEqual({
device: DEVICE_OBJECT,
expect(getPathComponents(PATH_CAMERA)).toEqual({
camera: CAMERA_OBJECT,
});
expect(getPathComponents(PATH_DEVICE_SHARE)).toEqual({
device: DEVICE_OBJECT,
expect(getPathComponents(PATH_CAMERA_SHARE)).toEqual({
camera: CAMERA_OBJECT,
});
expect(getPathComponents(PATH_DEVICE_PHOTO)).toEqual({
expect(getPathComponents(PATH_CAMERA_PHOTO)).toEqual({
photoId: PHOTO_ID,
device: DEVICE_OBJECT,
camera: CAMERA_OBJECT,
});
expect(getPathComponents(PATH_DEVICE_PHOTO_SHARE)).toEqual({
expect(getPathComponents(PATH_CAMERA_PHOTO_SHARE)).toEqual({
photoId: PHOTO_ID,
device: DEVICE_OBJECT,
camera: CAMERA_OBJECT,
});
});
it('can be escaped', () => {
@ -113,10 +113,10 @@ describe('Paths', () => {
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);
// Device views
expect(getEscapePath(PATH_DEVICE)).toEqual(PATH_GRID);
expect(getEscapePath(PATH_DEVICE_SHARE)).toEqual(PATH_DEVICE);
expect(getEscapePath(PATH_DEVICE_PHOTO)).toEqual(PATH_DEVICE);
expect(getEscapePath(PATH_DEVICE_PHOTO_SHARE)).toEqual(PATH_DEVICE_PHOTO);
// Camera views
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);
});
});

View File

@ -1,14 +1,14 @@
import {
getPhotosCached,
getPhotosCountCached,
getUniqueDevicesCached,
getUniqueCamerasCached,
getUniqueTagsCached,
} from '@/cache';
import HeaderList from '@/components/HeaderList';
import MorePhotos from '@/components/MorePhotos';
import SiteGrid from '@/components/SiteGrid';
import { generateOgImageMetaForPhotos, getPhotosLimitForQuery } from '@/photo';
import PhotoDevice from '@/device/PhotoDevice';
import PhotoCamera from '@/camera/PhotoCamera';
import PhotoGrid from '@/photo/PhotoGrid';
import PhotosEmptyState from '@/photo/PhotosEmptyState';
import { MAX_PHOTOS_TO_SHOW_HOME } from '@/photo/image-response';
@ -36,12 +36,12 @@ export default async function GridPage({
photos,
count,
tags,
devices,
cameras,
] = await Promise.all([
getPhotosCached({ limit }),
getPhotosCountCached(),
getUniqueTagsCached(),
getUniqueDevicesCached(),
getUniqueCamerasCached(),
]);
const showMorePhotos = count > photos.length;
@ -65,13 +65,13 @@ export default async function GridPage({
showIcon={false}
/>)}
/>}
{devices.length > 0 && <HeaderList
title="Devices"
{cameras.length > 0 && <HeaderList
title="Cameras"
icon={<IoMdCamera size={13} />}
items={devices.map(({ deviceKey, device }) =>
<PhotoDevice
key={deviceKey}
device={device}
items={cameras.map(({ cameraKey, camera }) =>
<PhotoCamera
key={cameraKey}
camera={camera}
showIcon={false}
hideApple
/>)}

View File

@ -11,17 +11,21 @@ import {
} from '@/site/paths';
import PhotoDetailPage from '@/photo/PhotoDetailPage';
import { getPhotoCached, getPhotosCached } from '@/cache';
import { getPhotos, getUniqueDevices } from '@/services/postgres';
import { deviceFromPhoto } from '@/device';
import { getPhotos, getUniqueCameras } from '@/services/postgres';
import { cameraFromPhoto } from '@/camera';
interface PhotoCameraProps {
params: { photoId: string, camera: string }
}
export async function generateStaticParams() {
const params: { params: { photoId: string, device: string }}[] = [];
const params: PhotoCameraProps[] = [];
const devices = await getUniqueDevices();
devices.forEach(async ({ deviceKey, device }) => {
const photos = await getPhotos({ device });
const cameras = await getUniqueCameras();
cameras.forEach(async ({ cameraKey, camera }) => {
const photos = await getPhotos({ camera });
params.push(...photos.map(photo => ({
params: { photoId: photo.id, device: deviceKey },
params: { photoId: photo.id, camera: cameraKey },
})));
});
@ -29,10 +33,8 @@ export async function generateStaticParams() {
}
export async function generateMetadata({
params: { photoId },
}: {
params: { photoId: string, device: string }
}): Promise<Metadata> {
params: { photoId, camera },
}: PhotoCameraProps): Promise<Metadata> {
const photo = await getPhotoCached(photoId);
if (!photo) { return {}; }
@ -40,7 +42,11 @@ export async function generateMetadata({
const title = titleForPhoto(photo);
const description = descriptionForPhoto(photo);
const images = absolutePathForPhotoImage(photo);
const url = absolutePathForPhoto(photo, undefined, deviceFromPhoto(photo));
const url = absolutePathForPhoto(
photo,
undefined,
cameraFromPhoto(photo, camera),
);
return {
title,
@ -60,27 +66,26 @@ export async function generateMetadata({
};
}
export default async function PhotoDevicePage({
params: { photoId },
export default async function PhotoCameraPage({
params: { photoId, camera: cameraProp },
children,
}: {
params: { photoId: string, tag: string }
}: PhotoCameraProps & {
children: React.ReactNode
}) {
const photo = await getPhotoCached(photoId);
if (!photo) { redirect(PATH_ROOT); }
const device = deviceFromPhoto(photo);
const camera = cameraFromPhoto(photo, cameraProp);
const photos = await getPhotosCached({ device });
const photos = await getPhotosCached({ camera });
return <>
{children}
<PhotoDetailPage
photo={photo}
photos={photos}
device={device}
camera={camera}
/>
</>;
}

View File

@ -0,0 +1,36 @@
import { getPhotoCached } from '@/cache';
import { cameraFromPhoto } from '@/camera';
import PhotoShareModal from '@/photo/PhotoShareModal';
import { getPhotos, getUniqueCameras } from '@/services/postgres';
import { PATH_ROOT } from '@/site/paths';
import { redirect } from 'next/navigation';
interface PhotoCameraParams {
params: { photoId: string, camera: string }
}
export async function generateStaticParams() {
const params: PhotoCameraParams[] = [];
const cameras = await getUniqueCameras();
cameras.forEach(async ({ cameraKey, camera }) => {
const photos = await getPhotos({ camera });
params.push(...photos.map(photo => ({
params: { photoId: photo.id, camera: cameraKey },
})));
});
return params;
}
export default async function Share({
params: { photoId, camera: cameraProp },
}: PhotoCameraParams) {
const photo = await getPhotoCached(photoId);
if (!photo) { return redirect(PATH_ROOT); }
const camera = cameraFromPhoto(photo, cameraProp);
return <PhotoShareModal {...{ photo, camera }} />;
}

View File

@ -1,11 +1,11 @@
import { auth } from '@/auth';
import { getImageCacheHeadersForAuth, getPhotosCached } from '@/cache';
import { getMakeModelFromDeviceString } from '@/device';
import { getMakeModelFromCameraString } from '@/camera';
import {
IMAGE_OG_SMALL_SIZE,
MAX_PHOTOS_TO_SHOW_PER_TAG,
} from '@/photo/image-response';
import DeviceImageResponse from '@/photo/image-response/DeviceImageResponse';
import CameraImageResponse from '@/photo/image-response/CameraImageResponse';
import { getIBMPlexMonoMedium } from '@/site/font';
import { ImageResponse } from 'next/server';
@ -13,9 +13,9 @@ export const runtime = 'edge';
export async function GET(
_: Request,
context: { params: { device: string } },
context: { params: { camera: string } },
) {
const device = getMakeModelFromDeviceString(context.params.device);
const camera = getMakeModelFromCameraString(context.params.camera);
const [
photos,
@ -24,7 +24,7 @@ export async function GET(
] = await Promise.all([
getPhotosCached({
limit: MAX_PHOTOS_TO_SHOW_PER_TAG,
device,
camera: camera,
}),
getIBMPlexMonoMedium(),
getImageCacheHeadersForAuth(await auth()),
@ -33,8 +33,8 @@ export async function GET(
const { width, height } = IMAGE_OG_SMALL_SIZE;
return new ImageResponse(
<DeviceImageResponse {...{
device,
<CameraImageResponse {...{
camera,
photos,
width,
height,

View File

@ -0,0 +1,65 @@
import { getPhotosCached } from '@/cache';
import SiteGrid from '@/components/SiteGrid';
import CameraHeader from '@/camera/CameraHeader';
import { getMakeModelFromCameraString } from '@/camera';
import PhotoGrid from '@/photo/PhotoGrid';
import { getUniqueCameras } from '@/services/postgres';
import { Metadata } from 'next';
import { generateMetaForCamera } from '@/camera/meta';
interface CameraProps {
params: { camera: string }
}
export async function generateStaticParams() {
const cameras = await getUniqueCameras();
return cameras.map(({ cameraKey }): CameraProps => ({
params: { camera: cameraKey },
}));
}
export async function generateMetadata({
params,
}: CameraProps): Promise<Metadata> {
const camera = getMakeModelFromCameraString(params.camera);
const photos = await getPhotosCached({ camera });
const {
url,
title,
description,
images,
} = generateMetaForCamera(camera, photos);
return {
title,
openGraph: {
title,
description,
images,
url,
},
twitter: {
images,
description,
card: 'summary_large_image',
},
description,
};
}
export default async function CameraPage({ params }:CameraProps) {
const camera = getMakeModelFromCameraString(params.camera);
const photos = await getPhotosCached({ camera });
return (
<SiteGrid
key="Camera Grid"
contentMain={<div className="space-y-8 mt-4">
<CameraHeader camera={camera} photos={photos} />
<PhotoGrid photos={photos} camera={camera} />
</div>}
/>
);
}

View File

@ -0,0 +1,70 @@
import { getPhotosCached } from '@/cache';
import SiteGrid from '@/components/SiteGrid';
import { cameraFromPhoto, getMakeModelFromCameraString } from '@/camera';
import CameraHeader from '@/camera/CameraHeader';
import CameraShareModal from '@/camera/CameraShareModal';
import { generateMetaForCamera } from '@/camera/meta';
import PhotoGrid from '@/photo/PhotoGrid';
import { getUniqueCameras } from '@/services/postgres';
import { Metadata } from 'next';
interface CameraProps {
params: { camera: string }
}
export async function generateStaticParams() {
const camera = await getUniqueCameras();
return camera.map(({ cameraKey }): CameraProps => ({
params: { camera: cameraKey },
}));
}
export async function generateMetadata({
params,
}: CameraProps): Promise<Metadata> {
const camera = getMakeModelFromCameraString(params.camera);
const photos = await getPhotosCached({ camera });
const {
url,
title,
description,
images,
} = generateMetaForCamera(camera, photos);
return {
title,
openGraph: {
title,
description,
images,
url,
},
twitter: {
images,
description,
card: 'summary_large_image',
},
description,
};
}
export default async function Share({ params }: CameraProps) {
const cameraFromParams = getMakeModelFromCameraString(params.camera);
const photos = await getPhotosCached({ camera: cameraFromParams });
const camera = cameraFromPhoto(photos[0], cameraFromParams);
return <>
<CameraShareModal {...{ camera, photos }} />
<SiteGrid
key="Camera Grid"
contentMain={<div className="space-y-8 mt-4">
<CameraHeader camera={camera} photos={photos} />
<PhotoGrid photos={photos} camera={camera} />
</div>}
/>
</>;
}

View File

@ -1,34 +0,0 @@
import { getPhotoCached } from '@/cache';
import { deviceFromPhoto } from '@/device';
import PhotoShareModal from '@/photo/PhotoShareModal';
import { getPhotos, getUniqueDevices } from '@/services/postgres';
import { PATH_ROOT } from '@/site/paths';
import { redirect } from 'next/navigation';
export async function generateStaticParams() {
const params: { params: { photoId: string, device: string }}[] = [];
const devices = await getUniqueDevices();
devices.forEach(async ({ deviceKey, device }) => {
const photos = await getPhotos({ device });
params.push(...photos.map(photo => ({
params: { photoId: photo.id, device: deviceKey },
})));
});
return params;
}
export default async function Share({
params: { photoId },
}: {
params: { photoId: string }
}) {
const photo = await getPhotoCached(photoId);
if (!photo) { return redirect(PATH_ROOT); }
const device = deviceFromPhoto(photo);
return <PhotoShareModal {...{ photo, device }} />;
}

View File

@ -1,65 +0,0 @@
import { getPhotosCached } from '@/cache';
import SiteGrid from '@/components/SiteGrid';
import DeviceHeader from '@/device/DeviceHeader';
import { getMakeModelFromDeviceString } from '@/device';
import PhotoGrid from '@/photo/PhotoGrid';
import { getUniqueDevices } from '@/services/postgres';
import { Metadata } from 'next';
import { generateMetaForDevice } from '@/device/meta';
interface DeviceProps {
params: { device: string }
}
export async function generateStaticParams() {
const devices = await getUniqueDevices();
return devices.map(({ deviceKey }): DeviceProps => ({
params: { device: deviceKey },
}));
}
export async function generateMetadata({
params,
}: DeviceProps): Promise<Metadata> {
const device = getMakeModelFromDeviceString(params.device);
const photos = await getPhotosCached({ device });
const {
url,
title,
description,
images,
} = generateMetaForDevice(device, photos);
return {
title,
openGraph: {
title,
description,
images,
url,
},
twitter: {
images,
description,
card: 'summary_large_image',
},
description,
};
}
export default async function DevicePage({ params }:DeviceProps) {
const device = getMakeModelFromDeviceString(params.device);
const photos = await getPhotosCached({ device });
return (
<SiteGrid
key="Device Grid"
contentMain={<div className="space-y-8 mt-4">
<DeviceHeader device={device} photos={photos} />
<PhotoGrid photos={photos} device={device} />
</div>}
/>
);
}

View File

@ -1,74 +0,0 @@
import { getPhotosCached } from '@/cache';
import SiteGrid from '@/components/SiteGrid';
import { deviceFromPhoto, getMakeModelFromDeviceString } from '@/device';
import DeviceHeader from '@/device/DeviceHeader';
import DeviceShareModal from '@/device/DeviceShareModal';
import { generateMetaForDevice } from '@/device/meta';
import PhotoGrid from '@/photo/PhotoGrid';
import { getUniqueDevices } from '@/services/postgres';
import { Metadata } from 'next';
interface DeviceProps {
params: { device: string }
}
export async function generateStaticParams() {
const devices = await getUniqueDevices();
return devices.map(({ deviceKey }): DeviceProps => ({
params: { device: deviceKey },
}));
}
export async function generateMetadata({
params,
}: DeviceProps): Promise<Metadata> {
const device = getMakeModelFromDeviceString(params.device);
const photos = await getPhotosCached({ device });
const {
url,
title,
description,
images,
} = generateMetaForDevice(device, photos);
return {
title,
openGraph: {
title,
description,
images,
url,
},
twitter: {
images,
description,
card: 'summary_large_image',
},
description,
};
}
export default async function Share({
params,
}: {
params: { device: string }
}) {
const deviceFromParams = getMakeModelFromDeviceString(params.device);
const photos = await getPhotosCached({ device: deviceFromParams });
const device = deviceFromPhoto(photos[0]) ?? deviceFromParams;
return <>
<DeviceShareModal {...{ device, photos }} />
<SiteGrid
key="Device Grid"
contentMain={<div className="space-y-8 mt-4">
<DeviceHeader device={device} photos={photos} />
<PhotoGrid photos={photos} device={device} />
</div>}
/>
</>;
}

20
src/cache/index.ts vendored
View File

@ -5,7 +5,7 @@ import {
getPhotos,
getPhotosCount,
getPhotosCountIncludingHidden,
getUniqueDevices,
getUniqueCameras,
getUniqueTags,
} from '@/services/postgres';
import { parseCachedPhotosDates, parseCachedPhotoDates } from '@/photo';
@ -15,7 +15,7 @@ import { AuthSession } from 'next-auth';
const TAG_PHOTOS = 'photos';
const TAG_PHOTOS_COUNT = 'photos-count';
const TAG_TAGS = 'tags';
const TAG_DEVICES = 'devices';
const TAG_CAMERAS = 'cameras';
const TAG_BLOB = 'blob';
// eslint-disable-next-line max-len
@ -40,7 +40,7 @@ const getPhotosCacheTagForKey = (
return value ? `${key}-${value.toISOString()}` : null;
}
// Complex keys
case 'device': {
case 'camera': {
const value = options[key];
return value ? `${key}-${value.make}-${value.model}` : null;
}
@ -66,8 +66,8 @@ export const revalidatePhotosTag = () =>
export const revalidateTagsTag = () =>
revalidateTag(TAG_TAGS);
export const revalidateDevicesTag = () =>
revalidateTag(TAG_DEVICES);
export const revalidateCamerasTag = () =>
revalidateTag(TAG_CAMERAS);
export const revalidateBlobTag = () =>
revalidateTag(TAG_BLOB);
@ -80,7 +80,7 @@ export const revalidatePhotosAndBlobTag = () => {
export const revalidateAllTags = () => {
revalidatePhotosTag();
revalidateTagsTag();
revalidateDevicesTag();
revalidateCamerasTag();
revalidateBlobTag();
};
@ -125,11 +125,11 @@ export const getUniqueTagsCached: typeof getUniqueTags = (...args) =>
}
)();
export const getUniqueDevicesCached: typeof getUniqueDevices = (...args) =>
export const getUniqueCamerasCached: typeof getUniqueCameras = (...args) =>
unstable_cache(
() => getUniqueDevices(...args),
[TAG_PHOTOS, TAG_DEVICES], {
tags: [TAG_PHOTOS, TAG_DEVICES],
() => getUniqueCameras(...args),
[TAG_PHOTOS, TAG_CAMERAS], {
tags: [TAG_PHOTOS, TAG_CAMERAS],
}
)();

View File

@ -0,0 +1,28 @@
import { Photo } from '@/photo';
import { pathForCameraShare } from '@/site/paths';
import PhotoHeader from '@/photo/PhotoHeader';
import { Camera, cameraFromPhoto } from '.';
import PhotoCamera from './PhotoCamera';
import { descriptionForCameraPhotos } from './meta';
export default function CameraHeader({
camera: cameraProp,
photos,
selectedPhoto,
}: {
camera: Camera
photos: Photo[]
selectedPhoto?: Photo
}) {
const camera = cameraFromPhoto(photos[0], cameraProp);
return (
<PhotoHeader
entity={<PhotoCamera {...{ camera }} />}
entityVerb="Photo"
entityDescription={descriptionForCameraPhotos(photos)}
photos={photos}
selectedPhoto={selectedPhoto}
sharePath={pathForCameraShare(camera)}
/>
);
}

View File

@ -1,13 +1,13 @@
import { Photo } from '@/photo';
import { absolutePathForDeviceImage, pathForDevice } from '@/site/paths';
import { absolutePathForCameraImage, pathForCamera } from '@/site/paths';
import OGTile from '@/components/OGTile';
import { Device, titleForDevice } from '.';
import { descriptionForDevicePhotos } from './meta';
import { Camera, titleForCamera } from '.';
import { descriptionForCameraPhotos } from './meta';
export type OGLoadingState = 'unloaded' | 'loading' | 'loaded' | 'failed';
export default function DeviceOGTile({
device,
export default function CameraOGTile({
camera,
photos,
loadingState: loadingStateExternal,
riseOnHover,
@ -15,7 +15,7 @@ export default function DeviceOGTile({
onFail,
retryTime,
}: {
device: Device
camera: Camera
photos: Photo[]
loadingState?: OGLoadingState
onLoad?: () => void
@ -25,10 +25,10 @@ export default function DeviceOGTile({
}) {
return (
<OGTile {...{
title: titleForDevice(device, photos),
description: descriptionForDevicePhotos(photos, true),
path: pathForDevice(device),
pathImageAbsolute: absolutePathForDeviceImage(device),
title: titleForCamera(camera, photos),
description: descriptionForCameraPhotos(photos, true),
path: pathForCamera(camera),
pathImageAbsolute: absolutePathForCameraImage(camera),
loadingState: loadingStateExternal,
onLoad,
onFail,

View File

@ -0,0 +1,23 @@
import { absolutePathForCamera, pathForCamera } from '@/site/paths';
import { Photo } from '../photo';
import ShareModal from '@/components/ShareModal';
import CameraOGTile from './CameraOGTile';
import { Camera } from '.';
export default function CameraShareModal({
camera,
photos,
}: {
camera: Camera
photos: Photo[]
}) {
return (
<ShareModal
title="Share Photos"
pathShare={absolutePathForCamera(camera)}
pathClose={pathForCamera(camera)}
>
<CameraOGTile {...{ camera, photos }} />
</ShareModal>
);
};

View File

@ -1,22 +1,22 @@
import { AiFillApple } from 'react-icons/ai';
import { cc } from '@/utility/css';
import Link from 'next/link';
import { pathForDevice } from '@/site/paths';
import { pathForCamera } from '@/site/paths';
import { IoMdCamera } from 'react-icons/io';
import { Device } from '.';
import { Camera } from '.';
export default function PhotoDevice({
device,
export default function PhotoCamera({
camera,
showIcon = true,
hideApple = true,
}: {
device: Device
camera: Camera
showIcon?: boolean
hideApple?: boolean
}) {
return (
<Link
href={pathForDevice(device)}
href={pathForCamera(camera)}
className={cc(
'inline-flex items-center self-start',
'uppercase',
@ -27,18 +27,18 @@ export default function PhotoDevice({
<IoMdCamera size={13} />
&nbsp;
</>}
{!(hideApple && device.make?.toLowerCase() === 'apple') &&
{!(hideApple && camera.make?.toLowerCase() === 'apple') &&
<>
{device.make?.toLowerCase() === 'apple'
{camera.make?.toLowerCase() === 'apple'
? <AiFillApple
title="Apple"
className="translate-y-[-0.5px]"
size={14}
/>
: device.make}
: camera.make}
&nbsp;
</>}
{device.model}
{camera.model}
</Link>
);
}

31
src/camera/index.ts Normal file
View File

@ -0,0 +1,31 @@
import { Photo } from '@/photo';
import { parameterize } from '@/utility/string';
const CAMERA_PLACEHOLDER: Camera = { make: 'Camera', model: 'Model' };
export type Camera = {
make: string
model: string
};
export const createCameraKey = (make: string, model: string) =>
parameterize(`${make}-${model}`);
// Assumes no makes ('Fujifilm,' 'Apple,' 'Canon', etc.) have dashes
export const getMakeModelFromCameraString = (camera: string): Camera => {
const [make, model] = camera.toLowerCase().split(/[-| ](.*)/s);
return { make, model };
};
export const cameraFromPhoto = (
photo: Photo | undefined,
fallback?: Camera | string,
): Camera =>
photo?.make && photo?.model
? { make: photo.make, model: photo.model }
: typeof fallback === 'string'
? getMakeModelFromCameraString(fallback)
: fallback ?? CAMERA_PLACEHOLDER;
export const formatCameraText = ({ make, model }: Camera) =>
`${make} ${model}`;

35
src/camera/meta.ts Normal file
View File

@ -0,0 +1,35 @@
import { Photo, descriptionForPhotoSet, photoQuantityText } from '@/photo';
import { Camera, cameraFromPhoto, formatCameraText } from '.';
import {
absolutePathForCamera,
absolutePathForCameraImage,
} from '@/site/paths';
// Meta functions moved to separate file to avoid
// dependencies (camelcase-keys) found in photo/index.ts
// which cause Jest to crash
export const titleForCamera = (
camera: Camera,
photos: Photo[],
) => [
'Shot on',
formatCameraText(cameraFromPhoto(photos[0], camera)),
photoQuantityText(photos),
].join(' ');
export const descriptionForCameraPhotos = (
photos: Photo[],
dateBased?: boolean,
) =>
descriptionForPhotoSet(photos, 'camera', dateBased);
export const generateMetaForCamera = (
camera: Camera,
photos: Photo[]
) => ({
url: absolutePathForCamera(camera),
title: titleForCamera(camera, photos),
description: descriptionForCameraPhotos(photos, true),
images: absolutePathForCameraImage(camera),
});

View File

@ -1,28 +0,0 @@
import { Photo } from '@/photo';
import { pathForDeviceShare } from '@/site/paths';
import PhotoHeader from '@/photo/PhotoHeader';
import { Device, formatDevice } from '.';
import PhotoDevice from './PhotoDevice';
import { descriptionForDevicePhotos } from './meta';
export default function DeviceHeader({
device: deviceFromProps,
photos,
selectedPhoto,
}: {
device: Device
photos: Photo[]
selectedPhoto?: Photo
}) {
const device = formatDevice(deviceFromProps, photos[0]);
return (
<PhotoHeader
entity={<PhotoDevice {...{ device }} />}
entityVerb="Device"
entityDescription={descriptionForDevicePhotos(photos)}
photos={photos}
selectedPhoto={selectedPhoto}
sharePath={pathForDeviceShare(device)}
/>
);
}

View File

@ -1,23 +0,0 @@
import { absolutePathForDevice, pathForDevice } from '@/site/paths';
import { Photo } from '../photo';
import ShareModal from '@/components/ShareModal';
import DeviceOGTile from './DeviceOGTile';
import { Device } from '.';
export default function DeviceShareModal({
device,
photos,
}: {
device: Device
photos: Photo[]
}) {
return (
<ShareModal
title="Share Photos"
pathShare={absolutePathForDevice(device)}
pathClose={pathForDevice(device)}
>
<DeviceOGTile {...{ device, photos }} />
</ShareModal>
);
};

View File

@ -1,40 +0,0 @@
import { Photo } from '@/photo';
import { capitalizeWords, parameterize } from '@/utility/string';
export type Device = {
make: string
model: string
};
export const createDeviceKey = (make: string, model: string) =>
parameterize(`${make}-${model}`);
export const titleForDevice = (
{ make, model }: Device,
photos:Photo[],
) => [
'Shot on',
deviceTextFromPhoto(photos[0]) ?? capitalizeWords(`${make} ${model}`),
].join(' ');
// Assumes no device makes ('Fujifilm,' 'Apple,' 'Canon', etc.)
// will have dashes in them
export const getMakeModelFromDeviceString = (device: string): Device => {
const [make, model] = device.toLowerCase().split(/[-| ](.*)/s);
return { make, model };
};
// Used to harvest original make/model with proper spaces/hyphens
export const deviceTextFromPhoto = (photo?: Photo) => photo
? `${photo.make} ${photo.model}`
: undefined;
export const deviceFromPhoto = (photo?: Photo): Device | undefined =>
photo?.make && photo?.model
? { make: photo.make, model: photo.model }
: undefined;
export const formatDevice = (device: Device, photo?: Photo): Device =>
photo?.make && photo?.model
? { make: photo.make, model: photo.model }
: device;

View File

@ -1,26 +0,0 @@
import { Photo, descriptionForPhotoSet } from '@/photo';
import { Device, titleForDevice } from '.';
import {
absolutePathForDevice,
absolutePathForDeviceImage,
} from '@/site/paths';
// Meta functions moved to separate file to avoid
// dependencies (camelcase-keys) found in photo/index.ts
// that cause Jest to crash
export const descriptionForDevicePhotos = (
photos: Photo[],
dateBased?: boolean,
) =>
descriptionForPhotoSet(photos, 'device', dateBased);
export const generateMetaForDevice = (
device: Device,
photos: Photo[]
) => ({
url: absolutePathForDevice(device),
title: titleForDevice(device, photos),
description: descriptionForDevicePhotos(photos, true),
images: absolutePathForDeviceImage(device),
});

View File

@ -6,21 +6,21 @@ import PhotoGrid from './PhotoGrid';
import { cc } from '@/utility/css';
import PhotoLinks from './PhotoLinks';
import TagHeader from '@/tag/TagHeader';
import { Device } from '@/device';
import DeviceHeader from '@/device/DeviceHeader';
import { Camera } from '@/camera';
import CameraHeader from '@/camera/CameraHeader';
export default function PhotoDetailPage({
photo,
photos,
photosGrid,
tag,
device,
camera,
}: {
photo: Photo
photos: Photo[]
photosGrid?: Photo[]
tag?: string
device?: Device
camera?: Camera
}) {
return (
<div>
@ -35,13 +35,13 @@ export default function PhotoDetailPage({
selectedPhoto={photo}
/>}
/>}
{device &&
{camera &&
<SiteGrid
className="mt-4 mb-8"
contentMain={
<DeviceHeader
<CameraHeader
key={tag}
device={device}
camera={camera}
photos={photos}
selectedPhoto={photo}
/>}
@ -56,9 +56,9 @@ export default function PhotoDetailPage({
tag={tag}
priority
prefetchShare
shareDevice={device !== undefined}
shareCamera={camera !== undefined}
shouldScrollOnShare={false}
showDevice={false}
showCamera={!camera}
/>,
]}
/>
@ -79,7 +79,7 @@ export default function PhotoDetailPage({
photo,
photos,
tag,
device,
camera,
}} />
</div>}
/>

View File

@ -2,13 +2,13 @@ import { Photo } from '.';
import PhotoSmall from './PhotoSmall';
import { cc } from '@/utility/css';
import AnimateItems from '@/components/AnimateItems';
import { Device } from '@/device';
import { Camera } from '@/camera';
export default function PhotoGrid({
photos,
selectedPhoto,
tag,
device,
camera,
fast,
animate = true,
animateOnFirstLoadOnly,
@ -17,7 +17,7 @@ export default function PhotoGrid({
photos: Photo[]
selectedPhoto?: Photo
tag?: string
device?: Device
camera?: Camera
fast?: boolean
animate?: boolean
animateOnFirstLoadOnly?: boolean
@ -41,7 +41,7 @@ export default function PhotoGrid({
key={photo.id}
photo={photo}
tag={tag}
device={device}
camera={camera}
selected={photo.id === selectedPhoto?.id}
/>)}
/>

View File

@ -6,29 +6,30 @@ import Link from 'next/link';
import { pathForPhoto, pathForPhotoShare } from '@/site/paths';
import PhotoTags from '@/tag/PhotoTags';
import ShareButton from '@/components/ShareButton';
import PhotoDevice from '../device/PhotoDevice';
import { deviceFromPhoto } from '@/device';
import PhotoCamera from '../camera/PhotoCamera';
import { Camera, cameraFromPhoto } from '@/camera';
export default function PhotoLarge({
photo,
tag,
priority,
prefetchShare,
shareDevice,
shouldScrollOnShare,
showDevice = true,
showCamera = true,
shareCamera,
}: {
photo: Photo
tag?: string
camera?: Camera
priority?: boolean
prefetchShare?: boolean
shareDevice?: boolean
shouldScrollOnShare?: boolean
showDevice?: boolean
showCamera?: boolean
shareCamera?: boolean
}) {
const tagsToShow = photo.tags.filter(t => t !== tag);
const device = deviceFromPhoto(photo);
const camera = cameraFromPhoto(photo);
const renderMiniGrid = (children: JSX.Element) =>
<div className={cc(
@ -71,9 +72,9 @@ export default function PhotoLarge({
{tagsToShow.length > 0 &&
<PhotoTags tags={tagsToShow} />}
</div>
{showDevice && device &&
<PhotoDevice
device={device}
{showCamera &&
<PhotoCamera
camera={camera}
showIcon={false}
hideApple={false}
/>}
@ -114,7 +115,7 @@ export default function PhotoLarge({
path={pathForPhotoShare(
photo,
tag,
shareDevice ? device : undefined,
shareCamera ? camera : undefined,
)}
prefetch={prefetchShare}
shouldScroll={shouldScrollOnShare}

View File

@ -6,19 +6,19 @@ import Link from 'next/link';
import { AnimationConfig } from '../components/AnimateItems';
import { useAppState } from '@/state';
import { pathForPhoto } from '@/site/paths';
import { Device } from '@/device';
import { Camera } from '@/camera';
export default function PhotoLink({
photo,
tag,
device,
camera,
prefetch,
nextPhotoAnimation,
children,
}: {
photo?: Photo
tag?: string
device?: Device
camera?: Camera
prefetch?: boolean
nextPhotoAnimation?: AnimationConfig
children: ReactNode
@ -28,7 +28,7 @@ export default function PhotoLink({
return (
photo
? <Link
href={pathForPhoto(photo, tag, device)}
href={pathForPhoto(photo, tag, camera)}
prefetch={prefetch}
onClick={() => {
if (nextPhotoAnimation) {

View File

@ -7,7 +7,7 @@ import { useRouter } from 'next/navigation';
import { pathForPhoto } from '@/site/paths';
import { useAppState } from '@/state';
import { AnimationConfig } from '@/components/AnimateItems';
import { Device } from '@/device';
import { Camera } from '@/camera';
const LISTENER_KEYUP = 'keyup';
@ -18,12 +18,12 @@ export default function PhotoLinks({
photo,
photos,
tag,
device,
camera,
}: {
photo: Photo
photos: Photo[]
tag?: string
device?: Device
camera?: Camera
}) {
const router = useRouter();
@ -40,7 +40,7 @@ export default function PhotoLinks({
if (previousPhoto) {
setNextPhotoAnimation?.(ANIMATION_RIGHT);
router.push(
pathForPhoto(previousPhoto, tag, device),
pathForPhoto(previousPhoto, tag, camera),
{ scroll: false },
);
}
@ -50,7 +50,7 @@ export default function PhotoLinks({
if (nextPhoto) {
setNextPhotoAnimation?.(ANIMATION_LEFT);
router.push(
pathForPhoto(nextPhoto, tag, device),
pathForPhoto(nextPhoto, tag, camera),
{ scroll: false },
);
}
@ -65,7 +65,7 @@ export default function PhotoLinks({
previousPhoto,
nextPhoto,
tag,
device,
camera,
]);
return (
@ -74,7 +74,7 @@ export default function PhotoLinks({
photo={previousPhoto}
nextPhotoAnimation={ANIMATION_RIGHT}
tag={tag}
device={device}
camera={camera}
prefetch
>
PREV
@ -83,7 +83,7 @@ export default function PhotoLinks({
photo={nextPhoto}
nextPhotoAnimation={ANIMATION_LEFT}
tag={tag}
device={device}
camera={camera}
prefetch
>
NEXT

View File

@ -2,22 +2,22 @@ import PhotoOGTile from '@/photo/PhotoOGTile';
import { absolutePathForPhoto, pathForPhoto } from '@/site/paths';
import { Photo } from '.';
import ShareModal from '@/components/ShareModal';
import { Device } from '@/device';
import { Camera } from '@/camera';
export default function PhotoShareModal({
photo,
tag,
device,
camera,
}: {
photo: Photo
tag?: string
device?: Device
camera?: Camera
}) {
return (
<ShareModal
title="Share Photo"
pathShare={absolutePathForPhoto(photo, tag, device)}
pathClose={pathForPhoto(photo, tag, device)}
pathShare={absolutePathForPhoto(photo, tag, camera)}
pathClose={pathForPhoto(photo, tag, camera)}
>
<PhotoOGTile photo={photo} />
</ShareModal>

View File

@ -3,22 +3,22 @@ import ImageSmall from '@/components/ImageSmall';
import Link from 'next/link';
import { cc } from '@/utility/css';
import { pathForPhoto } from '@/site/paths';
import { Device } from '@/device';
import { Camera } from '@/camera';
export default function PhotoSmall({
photo,
tag,
device,
camera,
selected,
}: {
photo: Photo
tag?: string
device?: Device
camera?: Camera
selected?: boolean
}) {
return (
<Link
href={pathForPhoto(photo, tag, device)}
href={pathForPhoto(photo, tag, camera)}
className={cc(
'active:brightness-75',
selected && 'brightness-50',

View File

@ -2,23 +2,23 @@ import { Photo } from '..';
import ImageCaption from './components/ImageCaption';
import ImagePhotoGrid from './components/ImagePhotoGrid';
import ImageContainer from './components/ImageContainer';
import { Device, formatDevice } from '@/device';
import { Camera, cameraFromPhoto } from '@/camera';
import { IoMdCamera } from 'react-icons/io';
export default function DeviceImageResponse({
device: deviceFromProps,
export default function CameraImageResponse({
camera: cameraProp,
photos,
width,
height,
fontFamily,
}: {
device: Device
camera: Camera
photos: Photo[]
width: number
height: number
fontFamily: string
}) {
const { make, model } = formatDevice(deviceFromProps, photos[0]);
const { make, model } = cameraFromPhoto(photos[0], cameraProp);
return (
<ImageContainer {...{
width,

View File

@ -169,9 +169,12 @@ export const translatePhotoId = (id: string) =>
export const titleForPhoto = (photo: Photo) =>
photo.title || 'Untitled';
export const labelForPhotos = (photos: Photo[]) =>
const labelForPhotos = (photos: Photo[]) =>
photos.length === 1 ? 'Photo' : 'Photos';
export const photoQuantityText = (photos: Photo[]) =>
`(${photos.length} ${labelForPhotos(photos)})`;
export const descriptionForPhotoSet = (
photos:Photo[],
descriptor: string,

View File

@ -6,7 +6,7 @@ import {
parsePhotoFromDb,
Photo,
} from '@/photo';
import { Device, createDeviceKey } from '@/device';
import { Camera, createCameraKey } from '@/camera';
import { parameterize } from '@/utility/string';
const PHOTO_DEFAULT_LIMIT = 100;
@ -190,7 +190,7 @@ const sqlGetPhotosByTag = (
LIMIT ${limit} OFFSET ${offset}
`;
const sqlGetPhotosByDevice = async (
const sqlGetPhotosByCamera = async (
limit = PHOTO_DEFAULT_LIMIT,
make: string,
model: string,
@ -245,13 +245,13 @@ const sqlGetUniqueTags = async () => sql`
ORDER BY tag ASC
`.then(({ rows }) => rows.map(row => row.tag as string));
const sqlGetUniqueDevices = async () => sql`
SELECT DISTINCT make||' '||model as device, make, model FROM photos
const sqlGetUniqueCameras = async () => sql`
SELECT DISTINCT make||' '||model as camera, make, model FROM photos
WHERE hidden IS NOT TRUE
ORDER BY device ASC
ORDER BY camera ASC
`.then(({ rows }) => rows.map(({ make, model }) => ({
deviceKey: createDeviceKey(make, model),
device: { make, model } as Device,
cameraKey: createCameraKey(make, model),
camera: { make, model } as Camera,
})));
export type GetPhotosOptions = {
@ -259,7 +259,7 @@ export type GetPhotosOptions = {
limit?: number
offset?: number
tag?: string
device?: Device
camera?: Camera
takenBefore?: Date
takenAfterInclusive?: Date
includeHidden?: boolean
@ -299,7 +299,7 @@ export const getPhotos = async (options: GetPhotosOptions = {}) => {
limit,
offset,
tag,
device,
camera,
takenBefore,
takenAfterInclusive,
includeHidden,
@ -316,8 +316,8 @@ export const getPhotos = async (options: GetPhotosOptions = {}) => {
getPhotosSql = () => sqlGetPhotosTakenAfterDateInclusive(takenAfterInclusive, limit);
} else if (tag) {
getPhotosSql = () => sqlGetPhotosByTag(limit, offset, tag);
} else if (device) {
getPhotosSql = () => sqlGetPhotosByDevice(limit, device.make, device.model);
} else if (camera) {
getPhotosSql = () => sqlGetPhotosByCamera(limit, camera.make, camera.model);
} else if (sortBy === 'createdAt') {
getPhotosSql = () => sqlGetPhotosSortedByCreatedAt(limit, offset);
} else if (sortBy === 'priority') {
@ -344,4 +344,4 @@ export const getPhotosCountIncludingHidden = () =>
export const getUniqueTags = () => safelyQueryPhotos(sqlGetUniqueTags);
export const getUniqueDevices = () => safelyQueryPhotos(sqlGetUniqueDevices);
export const getUniqueCameras = () => safelyQueryPhotos(sqlGetUniqueCameras);

View File

@ -1,15 +1,15 @@
import { Photo } from '@/photo';
import { BASE_URL } from './config';
import {
Device,
createDeviceKey,
getMakeModelFromDeviceString,
} from '@/device';
Camera,
createCameraKey,
getMakeModelFromCameraString,
} from '@/camera';
// Prefixes
const PREFIX_PHOTO = '/p';
const PREFIX_TAG = '/t';
const PREFIX_DEVICE = '/shot-on';
const PREFIX_CAMERA = '/shot-on';
// Modifiers
const SHARE = 'share';
@ -52,20 +52,20 @@ const getPhotoId = (photoOrPhotoId: PhotoOrPhotoId) =>
export const pathForPhoto = (
photo: PhotoOrPhotoId,
tag?: string,
device?: Device,
camera?: Camera,
) =>
tag
? `${pathForTag(tag)}/${getPhotoId(photo)}`
: device
? `${pathForDevice(device)}/${getPhotoId(photo)}`
: camera
? `${pathForCamera(camera)}/${getPhotoId(photo)}`
: `${PREFIX_PHOTO}/${getPhotoId(photo)}`;
export const pathForPhotoShare = (
photo: PhotoOrPhotoId,
tag?: string,
device?: Device,
camera?: Camera,
) =>
`${pathForPhoto(photo, tag, device)}/${SHARE}`;
`${pathForPhoto(photo, tag, camera)}/${SHARE}`;
export const pathForPhotoEdit = (photo: PhotoOrPhotoId) =>
`${PATH_ADMIN_PHOTOS}/${getPhotoId(photo)}/edit`;
@ -76,24 +76,24 @@ export const pathForTag = (tag: string) =>
export const pathForTagShare = (tag: string) =>
`${pathForTag(tag)}/${SHARE}`;
export const pathForDevice = ({ make, model }: Device) =>
`${PREFIX_DEVICE}/${createDeviceKey(make, model)}`;
export const pathForCamera = ({ make, model }: Camera) =>
`${PREFIX_CAMERA}/${createCameraKey(make, model)}`;
export const pathForDeviceShare = (device: Device) =>
`${pathForDevice(device)}/${SHARE}`;
export const pathForCameraShare = (camera: Camera) =>
`${pathForCamera(camera)}/${SHARE}`;
export const absolutePathForPhoto = (
photo: PhotoOrPhotoId,
tag?: string,
device?: Device,
camera?: Camera,
) =>
`${BASE_URL}${pathForPhoto(photo, tag, device)}`;
`${BASE_URL}${pathForPhoto(photo, tag, camera)}`;
export const absolutePathForTag = (tag: string) =>
`${BASE_URL}${pathForTag(tag)}`;
export const absolutePathForDevice= (device: Device) =>
`${BASE_URL}${pathForDevice(device)}`;
export const absolutePathForCamera= (camera: Camera) =>
`${BASE_URL}${pathForCamera(camera)}`;
export const absolutePathForPhotoImage = (photo: PhotoOrPhotoId) =>
`${absolutePathForPhoto(photo)}/image`;
@ -101,8 +101,8 @@ export const absolutePathForPhotoImage = (photo: PhotoOrPhotoId) =>
export const absolutePathForTagImage = (tag: string) =>
`${absolutePathForTag(tag)}/image`;
export const absolutePathForDeviceImage= (device: Device) =>
`${absolutePathForDevice(device)}/image`;
export const absolutePathForCameraImage= (camera: Camera) =>
`${absolutePathForCamera(camera)}/image`;
// p/[photoId]
export const isPathPhoto = (pathname = '') =>
@ -128,20 +128,20 @@ export const isPathTagPhoto = (pathname = '') =>
export const isPathTagPhotoShare = (pathname = '') =>
/^\/t\/[^/]+\/[^/]+\/share\/?$/.test(pathname);
// shot-on/[device]
export const isPathDevice = (pathname = '') =>
// shot-on/[camera]
export const isPathCamera = (pathname = '') =>
/^\/shot-on\/[^/]+\/?$/.test(pathname);
// shot-on/[device]/share
export const isPathDeviceShare = (pathname = '') =>
// shot-on/[camera]/share
export const isPathCameraShare = (pathname = '') =>
/^\/shot-on\/[^/]+\/share\/?$/.test(pathname);
// shot-on/[device]/[photoId]
export const isPathDevicePhoto = (pathname = '') =>
// shot-on/[camera]/[photoId]
export const isPathCameraPhoto = (pathname = '') =>
/^\/shot-on\/[^/]+\/[^/]+\/?$/.test(pathname);
// shot-on/[device]/[photoId]/share
export const isPathDevicePhotoShare = (pathname = '') =>
// shot-on/[camera]/[photoId]/share
export const isPathCameraPhotoShare = (pathname = '') =>
/^\/shot-on\/[^/]+\/[^/]+\/share\/?$/.test(pathname);
export const isPathGrid = (pathname = '') =>
@ -160,40 +160,40 @@ export const isPathProtected = (pathname = '') =>
export const getPathComponents = (pathname = ''): {
photoId?: string
tag?: string
device?: Device
camera?: Camera
} => {
const photoIdFromPhoto = pathname.match(/^\/p\/([^/]+)/)?.[1];
const photoIdFromTag = pathname.match(/^\/t\/[^/]+\/((?!share)[^/]+)/)?.[1];
// eslint-disable-next-line max-len
const photoIdFromDevice = pathname.match(/^\/shot-on\/[^/]+\/((?!share)[^/]+)/)?.[1];
const photoIdFromCamera = pathname.match(/^\/shot-on\/[^/]+\/((?!share)[^/]+)/)?.[1];
const tag = pathname.match(/^\/t\/([^/]+)/)?.[1];
const deviceString = pathname.match(/^\/shot-on\/([^/]+)/)?.[1];
const device = deviceString
? getMakeModelFromDeviceString(deviceString)
const cameraString = pathname.match(/^\/shot-on\/([^/]+)/)?.[1];
const camera = cameraString
? getMakeModelFromCameraString(cameraString)
: undefined;
return {
photoId: (
photoIdFromPhoto ||
photoIdFromTag ||
photoIdFromDevice
photoIdFromCamera
),
tag,
device,
camera,
};
};
export const getEscapePath = (pathname?: string) => {
const { photoId, tag, device } = getPathComponents(pathname);
const { photoId, tag, camera } = getPathComponents(pathname);
if (
(photoId && isPathPhoto(pathname)) ||
(tag && isPathTag(pathname)) ||
(device && isPathDevice(pathname))
(camera && isPathCamera(pathname))
) {
return PATH_GRID;
} else if (photoId && isPathTagPhotoShare(pathname)) {
return pathForPhoto(photoId, tag);
} else if (photoId && isPathDevicePhotoShare(pathname)) {
return pathForPhoto(photoId, undefined, device);
} else if (photoId && isPathCameraPhotoShare(pathname)) {
return pathForPhoto(photoId, undefined, camera);
} else if (photoId && isPathPhotoShare(pathname)) {
return pathForPhoto(photoId);
} else if (tag && (
@ -201,10 +201,10 @@ export const getEscapePath = (pathname?: string) => {
isPathTagShare(pathname)
)) {
return pathForTag(tag);
} else if (device && (
isPathDevicePhoto(pathname) ||
isPathDeviceShare(pathname)
} else if (camera && (
isPathCameraPhoto(pathname) ||
isPathCameraShare(pathname)
)) {
return pathForDevice(device);
return pathForCamera(camera);
}
};

View File

@ -1,10 +1,10 @@
import { Photo, descriptionForPhotoSet, labelForPhotos } from '@/photo';
import { Photo, descriptionForPhotoSet, photoQuantityText } from '@/photo';
import { absolutePathForTag, absolutePathForTagImage } from '@/site/paths';
import { capitalizeWords } from '@/utility/string';
export const titleForTag = (tag: string, photos:Photo[]) => [
capitalizeWords(tag.replaceAll('-', ' ')),
`(${photos.length} ${labelForPhotos(photos)})`,
photoQuantityText(photos),
].join(' ');
export const descriptionForTaggedPhotos = (