diff --git a/src/photo/PhotoLink.tsx b/src/photo/PhotoLink.tsx
index b881c8ac..ad2984b0 100644
--- a/src/photo/PhotoLink.tsx
+++ b/src/photo/PhotoLink.tsx
@@ -6,16 +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';
export default function PhotoLink({
photo,
tag,
+ device,
prefetch,
nextPhotoAnimation,
children,
}: {
photo?: Photo
tag?: string
+ device?: Device
prefetch?: boolean
nextPhotoAnimation?: AnimationConfig
children: ReactNode
@@ -25,7 +28,7 @@ export default function PhotoLink({
return (
photo
?
{
if (nextPhotoAnimation) {
diff --git a/src/photo/PhotoLinks.tsx b/src/photo/PhotoLinks.tsx
index ca01ca6f..fc62df2d 100644
--- a/src/photo/PhotoLinks.tsx
+++ b/src/photo/PhotoLinks.tsx
@@ -7,6 +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';
const LISTENER_KEYUP = 'keyup';
@@ -17,10 +18,12 @@ export default function PhotoLinks({
photo,
photos,
tag,
+ device,
}: {
photo: Photo
photos: Photo[]
tag?: string
+ device?: Device
}) {
const router = useRouter();
@@ -36,14 +39,20 @@ export default function PhotoLinks({
case 'J':
if (previousPhoto) {
setNextPhotoAnimation?.(ANIMATION_RIGHT);
- router.push(pathForPhoto(previousPhoto, tag), { scroll: false });
+ router.push(
+ pathForPhoto(previousPhoto, tag, device),
+ { scroll: false },
+ );
}
break;
case 'ARROWRIGHT':
case 'L':
if (nextPhoto) {
setNextPhotoAnimation?.(ANIMATION_LEFT);
- router.push(pathForPhoto(nextPhoto, tag), { scroll: false });
+ router.push(
+ pathForPhoto(nextPhoto, tag, device),
+ { scroll: false },
+ );
}
break;
};
@@ -56,6 +65,7 @@ export default function PhotoLinks({
previousPhoto,
nextPhoto,
tag,
+ device,
]);
return (
@@ -64,6 +74,7 @@ export default function PhotoLinks({
photo={previousPhoto}
nextPhotoAnimation={ANIMATION_RIGHT}
tag={tag}
+ device={device}
prefetch
>
PREV
@@ -72,6 +83,7 @@ export default function PhotoLinks({
photo={nextPhoto}
nextPhotoAnimation={ANIMATION_LEFT}
tag={tag}
+ device={device}
prefetch
>
NEXT
diff --git a/src/photo/PhotoShareModal.tsx b/src/photo/PhotoShareModal.tsx
index a78c5cb7..41004f79 100644
--- a/src/photo/PhotoShareModal.tsx
+++ b/src/photo/PhotoShareModal.tsx
@@ -2,19 +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';
export default function PhotoShareModal({
photo,
tag,
+ device,
}: {
photo: Photo
tag?: string
+ device?: Device
}) {
return (
diff --git a/src/photo/PhotoSmall.tsx b/src/photo/PhotoSmall.tsx
index a86e1b67..6b5b44e4 100644
--- a/src/photo/PhotoSmall.tsx
+++ b/src/photo/PhotoSmall.tsx
@@ -3,19 +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';
export default function PhotoSmall({
photo,
tag,
+ device,
selected,
}: {
photo: Photo
tag?: string
+ device?: Device
selected?: boolean
}) {
return (
+
+
+
+
+ {make.toLowerCase() !== 'apple' && make}
+ {model}
+
+
+
+ );
+}
diff --git a/src/photo/image-response/components/ImageCaption.tsx b/src/photo/image-response/components/ImageCaption.tsx
index a3a0234a..bfb55ae2 100644
--- a/src/photo/image-response/components/ImageCaption.tsx
+++ b/src/photo/image-response/components/ImageCaption.tsx
@@ -1,17 +1,21 @@
+import { ReactNode } from 'react';
+
export default function ImageCaption({
height,
- children,
fontFamily,
+ subhead,
+ children,
}: {
width: number
height: number
fontFamily: string
- children: React.ReactNode
+ subhead?: ReactNode
+ children: ReactNode
}) {
return (
- {children}
+ {subhead &&
+
+ {subhead}
+
}
+
+ {children}
+
);
}
diff --git a/src/services/postgres.ts b/src/services/postgres.ts
index 11d8475d..35d5c9b1 100644
--- a/src/services/postgres.ts
+++ b/src/services/postgres.ts
@@ -6,6 +6,8 @@ import {
parsePhotoFromDb,
Photo,
} from '@/photo';
+import { Device, createDeviceKey } from '@/device';
+import { parameterize } from '@/utility/string';
const PHOTO_DEFAULT_LIMIT = 100;
@@ -195,8 +197,8 @@ const sqlGetPhotosByDevice = async (
) => sql
`
SELECT * FROM photos
WHERE
- LOWER(make)=${make} AND
- LOWER(REPLACE(model, ' ', '-'))=${model}
+ LOWER(make)=${parameterize(make)} AND
+ LOWER(REPLACE(model, ' ', '-'))=${parameterize(model)}
ORDER BY taken_at DESC
LIMIT ${limit}
`;
@@ -247,15 +249,17 @@ const sqlGetUniqueDevices = async () => sql`
SELECT DISTINCT make||' '||model as device, make, model FROM photos
WHERE hidden IS NOT TRUE
ORDER BY device ASC
-`.then(({ rows }) =>
- rows as { device: string, make: string, model: string }[]);
+`.then(({ rows }) => rows.map(({ make, model }) => ({
+ deviceKey: createDeviceKey(make, model),
+ device: { make, model } as Device,
+ })));
export type GetPhotosOptions = {
sortBy?: 'createdAt' | 'takenAt' | 'priority'
limit?: number
offset?: number
tag?: string
- device?: { make: string, model: string }
+ device?: Device
takenBefore?: Date
takenAfterInclusive?: Date
includeHidden?: boolean
diff --git a/src/site/paths.ts b/src/site/paths.ts
index 911c5f83..af8a8e3d 100644
--- a/src/site/paths.ts
+++ b/src/site/paths.ts
@@ -1,11 +1,15 @@
import { Photo } from '@/photo';
import { BASE_URL } from './config';
-import { parameterize } from '@/utility/string';
+import {
+ Device,
+ createDeviceKey,
+ getMakeModelFromDeviceString,
+} from '@/device';
// Prefixes
const PREFIX_PHOTO = '/p';
const PREFIX_TAG = '/t';
-const PREFIX_DEVICE = '/d';
+const PREFIX_DEVICE = '/shot-on';
// Modifiers
const SHARE = 'share';
@@ -45,13 +49,23 @@ type PhotoOrPhotoId = Photo | string;
const getPhotoId = (photoOrPhotoId: PhotoOrPhotoId) =>
typeof photoOrPhotoId === 'string' ? photoOrPhotoId : photoOrPhotoId.id;
-export const pathForPhoto = (photo: PhotoOrPhotoId, tag?: string) =>
+export const pathForPhoto = (
+ photo: PhotoOrPhotoId,
+ tag?: string,
+ device?: Device,
+) =>
tag
? `${pathForTag(tag)}/${getPhotoId(photo)}`
- : `${PREFIX_PHOTO}/${getPhotoId(photo)}`;
+ : device
+ ? `${pathForDevice(device)}/${getPhotoId(photo)}`
+ : `${PREFIX_PHOTO}/${getPhotoId(photo)}`;
-export const pathForPhotoShare = (photo: PhotoOrPhotoId, tag?: string) =>
- `${pathForPhoto(photo, tag)}/${SHARE}`;
+export const pathForPhotoShare = (
+ photo: PhotoOrPhotoId,
+ tag?: string,
+ device?: Device,
+) =>
+ `${pathForPhoto(photo, tag, device)}/${SHARE}`;
export const pathForPhotoEdit = (photo: PhotoOrPhotoId) =>
`${PATH_ADMIN_PHOTOS}/${getPhotoId(photo)}/edit`;
@@ -59,24 +73,37 @@ export const pathForPhotoEdit = (photo: PhotoOrPhotoId) =>
export const pathForTag = (tag: string) =>
`${PREFIX_TAG}/${tag}`;
-export const pathForDevice = (make?: string, model?: string) =>
- `${PREFIX_DEVICE}/${parameterize(`${make}-${model}`)}`;
-
export const pathForTagShare = (tag: string) =>
`${pathForTag(tag)}/${SHARE}`;
-export const absolutePathForPhoto = (photo: PhotoOrPhotoId, tag?: string) =>
- `${BASE_URL}${pathForPhoto(photo, tag)}`;
+export const pathForDevice = ({ make, model }: Device) =>
+ `${PREFIX_DEVICE}/${createDeviceKey(make, model)}`;
+
+export const pathForDeviceShare = (device: Device) =>
+ `${pathForDevice(device)}/${SHARE}`;
+
+export const absolutePathForPhoto = (
+ photo: PhotoOrPhotoId,
+ tag?: string,
+ device?: Device,
+) =>
+ `${BASE_URL}${pathForPhoto(photo, tag, device)}`;
export const absolutePathForTag = (tag: string) =>
`${BASE_URL}${pathForTag(tag)}`;
+export const absolutePathForDevice= (device: Device) =>
+ `${BASE_URL}${pathForDevice(device)}`;
+
export const absolutePathForPhotoImage = (photo: PhotoOrPhotoId) =>
`${absolutePathForPhoto(photo)}/image`;
export const absolutePathForTagImage = (tag: string) =>
`${absolutePathForTag(tag)}/image`;
+export const absolutePathForDeviceImage= (device: Device) =>
+ `${absolutePathForDevice(device)}/image`;
+
// p/[photoId]
export const isPathPhoto = (pathname = '') =>
/^\/p\/[^/]+\/?$/.test(pathname);
@@ -85,22 +112,38 @@ export const isPathPhoto = (pathname = '') =>
export const isPathPhotoShare = (pathname = '') =>
/^\/p\/[^/]+\/share\/?$/.test(pathname);
-// t/[tagId]
+// t/[tag]
export const isPathTag = (pathname = '') =>
/^\/t\/[^/]+\/?$/.test(pathname);
-// t/[tagId]/share
+// t/[tag]/share
export const isPathTagShare = (pathname = '') =>
/^\/t\/[^/]+\/share\/?$/.test(pathname);
-// t/[tagId]/[photoId]
+// t/[tag]/[photoId]
export const isPathTagPhoto = (pathname = '') =>
/^\/t\/[^/]+\/[^/]+\/?$/.test(pathname);
-// t/[tagId]/[photoId]/share
+// t/[tag]/[photoId]/share
export const isPathTagPhotoShare = (pathname = '') =>
/^\/t\/[^/]+\/[^/]+\/share\/?$/.test(pathname);
+// shot-on/[device]
+export const isPathDevice = (pathname = '') =>
+ /^\/shot-on\/[^/]+\/?$/.test(pathname);
+
+// shot-on/[device]/share
+export const isPathDeviceShare = (pathname = '') =>
+ /^\/shot-on\/[^/]+\/share\/?$/.test(pathname);
+
+// shot-on/[device]/[photoId]
+export const isPathDevicePhoto = (pathname = '') =>
+ /^\/shot-on\/[^/]+\/[^/]+\/?$/.test(pathname);
+
+// shot-on/[device]/[photoId]/share
+export const isPathDevicePhotoShare = (pathname = '') =>
+ /^\/shot-on\/[^/]+\/[^/]+\/share\/?$/.test(pathname);
+
export const isPathGrid = (pathname = '') =>
pathname.startsWith(PATH_GRID);
@@ -117,33 +160,51 @@ export const isPathProtected = (pathname = '') =>
export const getPathComponents = (pathname = ''): {
photoId?: string
tag?: string
+ device?: Device
} => {
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 tag = pathname.match(/^\/t\/([^/]+)/)?.[1];
+ const deviceString = pathname.match(/^\/shot-on\/([^/]+)/)?.[1];
+ const device = deviceString
+ ? getMakeModelFromDeviceString(deviceString)
+ : undefined;
return {
photoId: (
photoIdFromPhoto ||
- photoIdFromTag
+ photoIdFromTag ||
+ photoIdFromDevice
),
tag,
+ device,
};
};
export const getEscapePath = (pathname?: string) => {
- const { photoId, tag } = getPathComponents(pathname);
+ const { photoId, tag, device } = getPathComponents(pathname);
if (
(photoId && isPathPhoto(pathname)) ||
- (tag && isPathTag(pathname))
+ (tag && isPathTag(pathname)) ||
+ (device && isPathDevice(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 && isPathPhotoShare(pathname)) {
return pathForPhoto(photoId);
- } else if (tag && (isPathTagPhoto(pathname) || isPathTagShare(pathname))) {
- return pathForTag(tag);
- } else if (tag && isPathTagShare(pathname)) {
+ } else if (tag && (
+ isPathTagPhoto(pathname) ||
+ isPathTagShare(pathname)
+ )) {
return pathForTag(tag);
+ } else if (device && (
+ isPathDevicePhoto(pathname) ||
+ isPathDeviceShare(pathname)
+ )) {
+ return pathForDevice(device);
}
};
diff --git a/src/tag/PhotoTag.tsx b/src/tag/PhotoTag.tsx
index 87bc5c68..6d016a74 100644
--- a/src/tag/PhotoTag.tsx
+++ b/src/tag/PhotoTag.tsx
@@ -12,9 +12,11 @@ export default function PhotoTag({
}) {
return (
{showIcon &&