Rename device to camera
This commit is contained in:
parent
886ff9224d
commit
69b61d1244
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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
|
||||
/>)}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
</>;
|
||||
}
|
||||
36
src/app/(static)/shot-on/[camera]/[photoId]/share/page.tsx
Normal file
36
src/app/(static)/shot-on/[camera]/[photoId]/share/page.tsx
Normal 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 }} />;
|
||||
}
|
||||
@ -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,
|
||||
65
src/app/(static)/shot-on/[camera]/page.tsx
Normal file
65
src/app/(static)/shot-on/[camera]/page.tsx
Normal 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>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
70
src/app/(static)/shot-on/[camera]/share/page.tsx
Normal file
70
src/app/(static)/shot-on/[camera]/share/page.tsx
Normal 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>}
|
||||
/>
|
||||
</>;
|
||||
}
|
||||
@ -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 }} />;
|
||||
}
|
||||
@ -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>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -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
20
src/cache/index.ts
vendored
@ -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],
|
||||
}
|
||||
)();
|
||||
|
||||
|
||||
28
src/camera/CameraHeader.tsx
Normal file
28
src/camera/CameraHeader.tsx
Normal 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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -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,
|
||||
23
src/camera/CameraShareModal.tsx
Normal file
23
src/camera/CameraShareModal.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -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} />
|
||||
|
||||
</>}
|
||||
{!(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}
|
||||
|
||||
</>}
|
||||
{device.model}
|
||||
{camera.model}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
31
src/camera/index.ts
Normal file
31
src/camera/index.ts
Normal 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
35
src/camera/meta.ts
Normal 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),
|
||||
});
|
||||
@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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;
|
||||
@ -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),
|
||||
});
|
||||
@ -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>}
|
||||
/>
|
||||
|
||||
@ -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}
|
||||
/>)}
|
||||
/>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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,
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@ -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 = (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user