diff --git a/__tests__/path.test.ts b/__tests__/path.test.ts
index c441c6fb..4263daef 100644
--- a/__tests__/path.test.ts
+++ b/__tests__/path.test.ts
@@ -1,3 +1,4 @@
+/* eslint-disable max-len */
import '@testing-library/jest-dom';
import {
getEscapePath,
@@ -6,6 +7,10 @@ import {
isPathCameraPhoto,
isPathCameraPhotoShare,
isPathCameraShare,
+ isPathFilmSimulation,
+ isPathFilmSimulationPhoto,
+ isPathFilmSimulationPhotoShare,
+ isPathFilmSimulationShare,
isPathPhoto,
isPathPhotoShare,
isPathTag,
@@ -15,28 +20,34 @@ import {
} from '@/site/paths';
import { getCameraFromKey } from '@/camera';
-const PHOTO_ID = 'UsKSGcbt';
-const TAG = 'tag-name';
-const CAMERA = 'fujifilm-x-t1';
-const CAMERA_OBJECT = getCameraFromKey(CAMERA);
-const SHARE = 'share';
+const PHOTO_ID = 'UsKSGcbt';
+const TAG = 'tag-name';
+const CAMERA = 'fujifilm-x-t1';
+const CAMERA_OBJECT = getCameraFromKey(CAMERA);
+const FILM_SIMULATION = 'acros';
+const SHARE = 'share';
-const PATH_ROOT = '/';
-const PATH_GRID = '/grid';
-const PATH_ADMIN = '/admin/photos';
+const PATH_ROOT = '/';
+const PATH_GRID = '/grid';
+const PATH_ADMIN = '/admin/photos';
-const PATH_PHOTO = `/p/${PHOTO_ID}`;
-const PATH_PHOTO_SHARE = `${PATH_PHOTO}/${SHARE}`;
+const PATH_PHOTO = `/p/${PHOTO_ID}`;
+const PATH_PHOTO_SHARE = `${PATH_PHOTO}/${SHARE}`;
-const PATH_TAG = `/tag/${TAG}`;
-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_TAG = `/tag/${TAG}`;
+const PATH_TAG_SHARE = `${PATH_TAG}/${SHARE}`;
+const PATH_TAG_PHOTO = `${PATH_TAG}/${PHOTO_ID}`;
+const PATH_TAG_PHOTO_SHARE = `${PATH_TAG_PHOTO}/${SHARE}`;
-const PATH_CAMERA = `/shot-on/${CAMERA}`;
-const PATH_CAMERA_SHARE = `${PATH_CAMERA}/${SHARE}`;
-const PATH_CAMERA_PHOTO = `${PATH_CAMERA}/${PHOTO_ID}`;
-const PATH_CAMERA_PHOTO_SHARE = `${PATH_CAMERA_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}`;
+
+const PATH_FILM_SIMULATION = `/film/${FILM_SIMULATION}`;
+const PATH_FILM_SIMULATION_SHARE = `${PATH_FILM_SIMULATION}/${SHARE}`;
+const PATH_FILM_SIMULATION_PHOTO = `${PATH_FILM_SIMULATION}/${PHOTO_ID}`;
+const PATH_FILM_SIMULATION_PHOTO_SHARE = `${PATH_FILM_SIMULATION_PHOTO}/${SHARE}`;
describe('Paths', () => {
it('can be classified', () => {
@@ -51,6 +62,10 @@ describe('Paths', () => {
expect(isPathCameraShare(PATH_CAMERA_SHARE)).toBe(true);
expect(isPathCameraPhoto(PATH_CAMERA_PHOTO)).toBe(true);
expect(isPathCameraPhotoShare(PATH_CAMERA_PHOTO_SHARE)).toBe(true);
+ expect(isPathFilmSimulation(PATH_FILM_SIMULATION)).toBe(true);
+ expect(isPathFilmSimulationShare(PATH_FILM_SIMULATION_SHARE)).toBe(true);
+ expect(isPathFilmSimulationPhoto(PATH_FILM_SIMULATION_PHOTO)).toBe(true);
+ expect(isPathFilmSimulationPhotoShare(PATH_FILM_SIMULATION_PHOTO_SHARE)).toBe(true);
// Negative
expect(isPathPhoto(PATH_TAG_PHOTO_SHARE)).toBe(false);
expect(isPathPhotoShare(PATH_TAG_PHOTO)).toBe(false);
@@ -62,6 +77,10 @@ describe('Paths', () => {
expect(isPathCameraShare(PATH_TAG)).toBe(false);
expect(isPathCameraPhoto(PATH_PHOTO_SHARE)).toBe(false);
expect(isPathCameraPhotoShare(PATH_PHOTO)).toBe(false);
+ expect(isPathFilmSimulation(PATH_TAG_SHARE)).toBe(false);
+ expect(isPathFilmSimulationShare(PATH_TAG)).toBe(false);
+ expect(isPathFilmSimulationPhoto(PATH_PHOTO_SHARE)).toBe(false);
+ expect(isPathFilmSimulationPhotoShare(PATH_PHOTO)).toBe(false);
});
it('can be parsed', () => {
expect(getPathComponents(PATH_ROOT)).toEqual({});
@@ -99,6 +118,20 @@ describe('Paths', () => {
photoId: PHOTO_ID,
camera: CAMERA_OBJECT,
});
+ expect(getPathComponents(PATH_FILM_SIMULATION)).toEqual({
+ simulation: FILM_SIMULATION,
+ });
+ expect(getPathComponents(PATH_FILM_SIMULATION_SHARE)).toEqual({
+ simulation: FILM_SIMULATION,
+ });
+ expect(getPathComponents(PATH_FILM_SIMULATION_PHOTO)).toEqual({
+ photoId: PHOTO_ID,
+ simulation: FILM_SIMULATION,
+ });
+ expect(getPathComponents(PATH_FILM_SIMULATION_PHOTO_SHARE)).toEqual({
+ photoId: PHOTO_ID,
+ simulation: FILM_SIMULATION,
+ });
});
it('can be escaped', () => {
// Root views
@@ -118,5 +151,10 @@ describe('Paths', () => {
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);
+ // Film Simulation views
+ expect(getEscapePath(PATH_FILM_SIMULATION)).toEqual(PATH_GRID);
+ expect(getEscapePath(PATH_FILM_SIMULATION_SHARE)).toEqual(PATH_FILM_SIMULATION);
+ expect(getEscapePath(PATH_FILM_SIMULATION_PHOTO)).toEqual(PATH_FILM_SIMULATION);
+ expect(getEscapePath(PATH_FILM_SIMULATION_PHOTO_SHARE)).toEqual(PATH_FILM_SIMULATION_PHOTO);
});
});
diff --git a/src/app/(static)/film/animate/page.tsx b/src/app/(static)/film-demo/animate/page.tsx
similarity index 92%
rename from src/app/(static)/film/animate/page.tsx
rename to src/app/(static)/film-demo/animate/page.tsx
index d7c70f97..e83b1199 100644
--- a/src/app/(static)/film/animate/page.tsx
+++ b/src/app/(static)/film-demo/animate/page.tsx
@@ -3,8 +3,8 @@
import SiteGrid from '@/components/SiteGrid';
import { cc } from '@/utility/css';
import { FILM_SIMULATION_FORM_INPUT_OPTIONS } from '@/vendors/fujifilm';
-import PhotoFujifilmSimulation from
- '@/vendors/fujifilm/PhotoFujifilmSimulation';
+import PhotoFilmSimulation from
+ '@/simulation/PhotoFilmSimulation';
import { useEffect, useState } from 'react';
export default function FilmPage() {
@@ -26,7 +26,7 @@ export default function FilmPage() {
Film Simulation:
-
{FILM_SIMULATION_FORM_INPUT_OPTIONS.map(({ value }) =>
-
diff --git a/src/app/(static)/film/[simulation]/[photoId]/layout.tsx b/src/app/(static)/film/[simulation]/[photoId]/layout.tsx
new file mode 100644
index 00000000..53b8a6d7
--- /dev/null
+++ b/src/app/(static)/film/[simulation]/[photoId]/layout.tsx
@@ -0,0 +1,85 @@
+import {
+ descriptionForPhoto,
+ titleForPhoto,
+} from '@/photo';
+import { Metadata } from 'next';
+import { redirect } from 'next/navigation';
+import {
+ PATH_ROOT,
+ absolutePathForPhoto,
+ absolutePathForPhotoImage,
+} from '@/site/paths';
+import PhotoDetailPage from '@/photo/PhotoDetailPage';
+import { getPhotoCached } from '@/cache';
+import { getPhotos, getUniqueFilmSimulations } from '@/services/postgres';
+import { ReactNode } from 'react';
+import { FilmSimulation } from '@/simulation';
+import { getPhotosFilmSimulationDataCached } from '@/simulation/data';
+
+interface PhotoFilmSimulationProps {
+ params: { photoId: string, simulation: FilmSimulation }
+}
+
+export async function generateStaticParams() {
+ const params: PhotoFilmSimulationProps[] = [];
+
+ const simulations = await getUniqueFilmSimulations();
+ simulations.forEach(async ({ simulation }) => {
+ const photos = await getPhotos({ simulation });
+ params.push(...photos.map(photo => ({
+ params: { photoId: photo.id, simulation },
+ })));
+ });
+
+ return params;
+}
+
+export async function generateMetadata({
+ params: { photoId, simulation },
+}: PhotoFilmSimulationProps): Promise
{
+ const photo = await getPhotoCached(photoId);
+
+ if (!photo) { return {}; }
+
+ const title = titleForPhoto(photo);
+ const description = descriptionForPhoto(photo);
+ const images = absolutePathForPhotoImage(photo);
+ const url = absolutePathForPhoto(photo, simulation);
+
+ return {
+ title,
+ description,
+ openGraph: {
+ title,
+ images,
+ description,
+ url,
+ },
+ twitter: {
+ title,
+ description,
+ images,
+ card: 'summary_large_image',
+ },
+ };
+}
+
+export default async function PhotoFilmSimulationPage({
+ params: { photoId, simulation },
+ children,
+}: PhotoFilmSimulationProps & { children: ReactNode }) {
+ const photo = await getPhotoCached(photoId);
+
+ if (!photo) { redirect(PATH_ROOT); }
+
+ const [
+ photos,
+ count,
+ dateRange,
+ ] = await getPhotosFilmSimulationDataCached({ simulation });
+
+ return <>
+ {children}
+
+ >;
+}
diff --git a/src/app/(static)/film/[simulation]/[photoId]/page.tsx b/src/app/(static)/film/[simulation]/[photoId]/page.tsx
new file mode 100644
index 00000000..67e08591
--- /dev/null
+++ b/src/app/(static)/film/[simulation]/[photoId]/page.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return null;
+}
diff --git a/src/app/(static)/film/[simulation]/[photoId]/share/page.tsx b/src/app/(static)/film/[simulation]/[photoId]/share/page.tsx
new file mode 100644
index 00000000..0b0b493f
--- /dev/null
+++ b/src/app/(static)/film/[simulation]/[photoId]/share/page.tsx
@@ -0,0 +1,34 @@
+import { getPhotoCached } from '@/cache';
+import PhotoShareModal from '@/photo/PhotoShareModal';
+import { getPhotos, getUniqueFilmSimulations } from '@/services/postgres';
+import { FilmSimulation } from '@/simulation';
+import { PATH_ROOT } from '@/site/paths';
+import { redirect } from 'next/navigation';
+
+interface PhotoFilmSimulationProps {
+ params: { photoId: string, simulation: FilmSimulation }
+}
+
+export async function generateStaticParams() {
+ const params: PhotoFilmSimulationProps[] = [];
+
+ const simulations = await getUniqueFilmSimulations();
+ simulations.forEach(async ({ simulation }) => {
+ const photos = await getPhotos({ simulation });
+ params.push(...photos.map(photo => ({
+ params: { photoId: photo.id, simulation },
+ })));
+ });
+
+ return params;
+}
+
+export default async function Share({
+ params: { photoId, simulation },
+}: PhotoFilmSimulationProps) {
+ const photo = await getPhotoCached(photoId);
+
+ if (!photo) { return redirect(PATH_ROOT); }
+
+ return ;
+}
diff --git a/src/app/(static)/film/[simulation]/image/route.tsx b/src/app/(static)/film/[simulation]/image/route.tsx
new file mode 100644
index 00000000..fafe45ad
--- /dev/null
+++ b/src/app/(static)/film/[simulation]/image/route.tsx
@@ -0,0 +1,43 @@
+import { auth } from '@/auth';
+import { getImageCacheHeadersForAuth, getPhotosCached } from '@/cache';
+import {
+ IMAGE_OG_SMALL_SIZE,
+ MAX_PHOTOS_TO_SHOW_PER_TAG,
+} from '@/photo/image-response';
+import FilmSimulationImageResponse from
+ '@/photo/image-response/FilmSimulationImageResponse';
+import { FilmSimulation } from '@/simulation';
+import { getIBMPlexMonoMedium } from '@/site/font';
+import { ImageResponse } from 'next/og';
+
+export const runtime = 'edge';
+
+export async function GET(
+ _: Request,
+ context: { params: { simulation: FilmSimulation } },
+) {
+ const { simulation } = context.params;
+
+ const [
+ photos,
+ { fontFamily, fonts },
+ headers,
+ ] = await Promise.all([
+ getPhotosCached({ limit: MAX_PHOTOS_TO_SHOW_PER_TAG, simulation }),
+ getIBMPlexMonoMedium(),
+ getImageCacheHeadersForAuth(await auth()),
+ ]);
+
+ const { width, height } = IMAGE_OG_SMALL_SIZE;
+
+ return new ImageResponse(
+ ,
+ { width, height, fonts, headers },
+ );
+}
diff --git a/src/app/(static)/film/[simulation]/page.tsx b/src/app/(static)/film/[simulation]/page.tsx
new file mode 100644
index 00000000..c80bbade
--- /dev/null
+++ b/src/app/(static)/film/[simulation]/page.tsx
@@ -0,0 +1,76 @@
+import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo';
+import { FilmSimulation, generateMetaForFilmSimulation } from '@/simulation';
+import FilmSimulationOverview from '@/simulation/FilmSimulationOverview';
+import {
+ getPhotosFilmSimulationDataCached,
+ getPhotosFilmSimulationDataCachedWithPagination,
+} from '@/simulation/data';
+import { PaginationParams } from '@/site/pagination';
+import { Metadata } from 'next';
+
+export const runtime = 'edge';
+
+interface FilmSimulationProps {
+ params: { simulation: FilmSimulation }
+}
+
+export async function generateMetadata({
+ params: { simulation },
+}: FilmSimulationProps): Promise {
+ const [
+ photos,
+ count,
+ dateRange,
+ ] = await getPhotosFilmSimulationDataCached({
+ simulation,
+ limit: GRID_THUMBNAILS_TO_SHOW_MAX,
+ });
+
+ const {
+ url,
+ title,
+ description,
+ images,
+ } = generateMetaForFilmSimulation(simulation, photos, count, dateRange);
+
+ return {
+ title,
+ openGraph: {
+ title,
+ description,
+ images,
+ url,
+ },
+ twitter: {
+ images,
+ description,
+ card: 'summary_large_image',
+ },
+ description,
+ };
+}
+
+export default async function FilmSimulationPage({
+ params: { simulation },
+ searchParams,
+}: FilmSimulationProps & PaginationParams) {
+ const {
+ photos,
+ count,
+ showMorePath,
+ dateRange,
+ } = await getPhotosFilmSimulationDataCachedWithPagination({
+ simulation,
+ searchParams,
+ });
+
+ return (
+
+ );
+}
diff --git a/src/app/(static)/film/[simulation]/share/page.tsx b/src/app/(static)/film/[simulation]/share/page.tsx
new file mode 100644
index 00000000..b26320c3
--- /dev/null
+++ b/src/app/(static)/film/[simulation]/share/page.tsx
@@ -0,0 +1,75 @@
+import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo';
+import { FilmSimulation, generateMetaForFilmSimulation } from '@/simulation';
+import FilmSimulationOverview from '@/simulation/FilmSimulationOverview';
+import FilmSimulationShareModal from '@/simulation/FilmSimulationShareModal';
+import {
+ getPhotosFilmSimulationDataCached,
+ getPhotosFilmSimulationDataCachedWithPagination,
+} from '@/simulation/data';
+import { PaginationParams } from '@/site/pagination';
+import { Metadata } from 'next';
+
+export const runtime = 'edge';
+
+interface FilmSimulationProps {
+ params: { simulation: FilmSimulation }
+}
+
+export async function generateMetadata({
+ params: { simulation },
+}: FilmSimulationProps): Promise {
+ const [
+ photos,
+ count,
+ dateRange,
+ ] = await getPhotosFilmSimulationDataCached({
+ simulation,
+ limit: GRID_THUMBNAILS_TO_SHOW_MAX,
+ });
+
+ const {
+ url,
+ title,
+ description,
+ images,
+ } = generateMetaForFilmSimulation(simulation, photos, count, dateRange);
+
+ return {
+ title,
+ openGraph: {
+ title,
+ description,
+ images,
+ url,
+ },
+ twitter: {
+ images,
+ description,
+ card: 'summary_large_image',
+ },
+ description,
+ };
+}
+
+export default async function Share({
+ params: { simulation },
+ searchParams,
+}: FilmSimulationProps & PaginationParams) {
+ const {
+ photos,
+ count,
+ dateRange,
+ showMorePath,
+ } = await getPhotosFilmSimulationDataCachedWithPagination({
+ simulation,
+ searchParams,
+ });
+
+ return <>
+
+
+ >;
+}
diff --git a/src/app/(static)/tag/[tag]/[photoId]/share/page.tsx b/src/app/(static)/tag/[tag]/[photoId]/share/page.tsx
index 85500bcd..0a892186 100644
--- a/src/app/(static)/tag/[tag]/[photoId]/share/page.tsx
+++ b/src/app/(static)/tag/[tag]/[photoId]/share/page.tsx
@@ -29,5 +29,5 @@ export default async function Share({
if (!photo) { return redirect(PATH_ROOT); }
- return ;
+ return ;
}
diff --git a/src/app/(static)/tag/[tag]/image/route.tsx b/src/app/(static)/tag/[tag]/image/route.tsx
index 1368faa5..76c76d5d 100644
--- a/src/app/(static)/tag/[tag]/image/route.tsx
+++ b/src/app/(static)/tag/[tag]/image/route.tsx
@@ -14,7 +14,7 @@ export async function GET(
_: Request,
context: { params: { tag: string } },
) {
- const tag = context.params.tag;
+ const { tag } = context.params;
const [
photos,
diff --git a/src/cache/index.ts b/src/cache/index.ts
index 2da06f8c..003efd97 100644
--- a/src/cache/index.ts
+++ b/src/cache/index.ts
@@ -21,7 +21,7 @@ import { getBlobPhotoUrls, getBlobUploadUrls } from '@/services/blob';
import type { Session } from 'next-auth';
import { Camera, createCameraKey } from '@/camera';
import { PATHS_ADMIN, PATHS_TO_CACHE } from '@/site/paths';
-import { FujifilmSimulation } from '@/vendors/fujifilm';
+import { FilmSimulation } from '@/simulation';
const KEY_PHOTOS = 'photos';
const KEY_PHOTOS_COUNT = `${KEY_PHOTOS}-count`;
@@ -82,7 +82,7 @@ const getPhotoTagCountKey = (tag: string) =>
const getPhotoCameraCountKey = (camera: Camera) =>
`${KEY_PHOTOS_COUNT}-${KEY_CAMERAS}-${createCameraKey(camera)}`;
-const getPhotoFilmSimulationCountKey = (simulation: FujifilmSimulation) =>
+const getPhotoFilmSimulationCountKey = (simulation: FilmSimulation) =>
`${KEY_PHOTOS_COUNT}-${KEY_FILM_SIMULATIONS}-${simulation}`;
const getPhotoTagDateRangeKey = (tag: string) =>
@@ -91,7 +91,7 @@ const getPhotoTagDateRangeKey = (tag: string) =>
const getPhotoCameraDateRangeKey = (camera: Camera) =>
`${KEY_PHOTOS_DATE_RANGE}-${KEY_CAMERAS}-${createCameraKey(camera)}`;
-const getPhotoFilmSimulationDateRangeKey = (simulation: FujifilmSimulation) =>
+const getPhotoFilmSimulationDateRangeKey = (simulation: FilmSimulation) =>
`${KEY_PHOTOS_DATE_RANGE}-${KEY_FILM_SIMULATIONS}-${simulation}`;
export const revalidatePhotosKey = () =>
diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx
index eebeb2d2..4286a108 100644
--- a/src/components/Badge.tsx
+++ b/src/components/Badge.tsx
@@ -4,10 +4,12 @@ export default function Badge({
children,
type = 'primary',
uppercase,
+ interactive,
}: {
children: React.ReactNode
type?: 'primary' | 'secondary' | 'text-only'
uppercase?: boolean
+ interactive?: boolean
}) {
const baseStyles = () => {
switch (type) {
@@ -21,6 +23,8 @@ export default function Badge({
'bg-gray-100 dark:bg-gray-800/60',
'text-medium',
'font-medium text-[0.7rem]',
+ interactive && 'hover:text-black dark:hover:text-white',
+ interactive && 'active:bg-gray-200 dark:active:bg-gray-700/60',
);
}
};
diff --git a/src/photo/PhotoDetailPage.tsx b/src/photo/PhotoDetailPage.tsx
index d21e23ef..0a809132 100644
--- a/src/photo/PhotoDetailPage.tsx
+++ b/src/photo/PhotoDetailPage.tsx
@@ -8,6 +8,8 @@ import PhotoLinks from './PhotoLinks';
import TagHeader from '@/tag/TagHeader';
import { Camera } from '@/camera';
import CameraHeader from '@/camera/CameraHeader';
+import { FilmSimulation } from '@/simulation';
+import FilmSimulationHeader from '@/simulation/FilmSimulationHeader';
export default function PhotoDetailPage({
photo,
@@ -15,6 +17,7 @@ export default function PhotoDetailPage({
photosGrid,
tag,
camera,
+ simulation,
count,
dateRange,
}: {
@@ -23,6 +26,7 @@ export default function PhotoDetailPage({
photosGrid?: Photo[]
tag?: string
camera?: Camera
+ simulation?: FilmSimulation
count?: number
dateRange?: PhotoDateRange
}) {
@@ -51,6 +55,18 @@ export default function PhotoDetailPage({
dateRange={dateRange}
/>}
/>}
+ {simulation &&
+ }
+ />}
,
]}
/>
diff --git a/src/photo/PhotoGrid.tsx b/src/photo/PhotoGrid.tsx
index 4607ed48..b5943d78 100644
--- a/src/photo/PhotoGrid.tsx
+++ b/src/photo/PhotoGrid.tsx
@@ -4,12 +4,14 @@ import { cc } from '@/utility/css';
import AnimateItems from '@/components/AnimateItems';
import { Camera } from '@/camera';
import MorePhotos from '@/photo/MorePhotos';
+import { FilmSimulation } from '@/simulation';
export default function PhotoGrid({
photos,
selectedPhoto,
tag,
camera,
+ simulation,
fast,
animate = true,
animateOnFirstLoadOnly,
@@ -22,6 +24,7 @@ export default function PhotoGrid({
selectedPhoto?: Photo
tag?: string
camera?: Camera
+ simulation?: FilmSimulation
fast?: boolean
animate?: boolean
animateOnFirstLoadOnly?: boolean
@@ -52,6 +55,7 @@ export default function PhotoGrid({
photo={photo}
tag={tag}
camera={camera}
+ simulation={simulation}
selected={photo.id === selectedPhoto?.id}
/>).concat(additionalTile ?? [])}
/>
diff --git a/src/photo/PhotoGridSidebar.tsx b/src/photo/PhotoGridSidebar.tsx
index 94377ee6..7aa1f951 100644
--- a/src/photo/PhotoGridSidebar.tsx
+++ b/src/photo/PhotoGridSidebar.tsx
@@ -6,11 +6,11 @@ import { FaTag } from 'react-icons/fa';
import { IoMdCamera } from 'react-icons/io';
import { photoQuantityText } from '.';
import { Tags } from '@/tag';
-import PhotoFujifilmSimulation from
- '@/vendors/fujifilm/PhotoFujifilmSimulation';
-import { FujifilmSimulations } from '@/vendors/fujifilm';
-import PhotoFujifilmSimulationIcon from
- '@/vendors/fujifilm/PhotoFujifilmSimulationIcon';
+import PhotoFilmSimulation from
+ '@/simulation/PhotoFilmSimulation';
+import PhotoFilmSimulationIcon from
+ '@/simulation/PhotoFilmSimulationIcon';
+import { FilmSimulations } from '@/simulation';
export default function PhotoGridSidebar({
tags,
@@ -20,7 +20,7 @@ export default function PhotoGridSidebar({
}: {
tags: Tags
cameras: Cameras
- simulations: FujifilmSimulations
+ simulations: FilmSimulations
photosCount: number
}) {
return (
@@ -53,17 +53,17 @@ export default function PhotoGridSidebar({
/>}
{simulations.length > 0 && }
- className="space-y-0.5"
- items={simulations.map(({ simulation }) =>
+ items={simulations.map(({ simulation, count }) =>
)}
diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx
index 93676ca9..acfc34f1 100644
--- a/src/photo/PhotoLarge.tsx
+++ b/src/photo/PhotoLarge.tsx
@@ -7,9 +7,9 @@ import { pathForPhoto, pathForPhotoShare } from '@/site/paths';
import PhotoTags from '@/tag/PhotoTags';
import ShareButton from '@/components/ShareButton';
import PhotoCamera from '../camera/PhotoCamera';
-import { Camera, cameraFromPhoto } from '@/camera';
-import PhotoFujifilmSimulation from
- '@/vendors/fujifilm/PhotoFujifilmSimulation';
+import { cameraFromPhoto } from '@/camera';
+import PhotoFilmSimulation from
+ '@/simulation/PhotoFilmSimulation';
export default function PhotoLarge({
photo,
@@ -18,15 +18,16 @@ export default function PhotoLarge({
prefetchShare,
shouldScrollOnShare,
showCamera = true,
+ showSimulation = true,
shareCamera,
}: {
photo: Photo
tag?: string
- camera?: Camera
priority?: boolean
prefetchShare?: boolean
shouldScrollOnShare?: boolean
showCamera?: boolean
+ showSimulation?: boolean
shareCamera?: boolean
}) {
const tagsToShow = photo.tags.filter(t => t !== tag);
@@ -81,9 +82,9 @@ export default function PhotoLarge({
showIcon={false}
hideApple={false}
/>
- {photo.filmSimulation &&
+ {showSimulation && photo.filmSimulation &&
}
@@ -125,6 +126,7 @@ export default function PhotoLarge({
photo,
tag,
shareCamera ? camera : undefined,
+ photo.filmSimulation,
)}
prefetch={prefetchShare}
shouldScroll={shouldScrollOnShare}
diff --git a/src/photo/PhotoShareModal.tsx b/src/photo/PhotoShareModal.tsx
index 90a92d18..69e155fc 100644
--- a/src/photo/PhotoShareModal.tsx
+++ b/src/photo/PhotoShareModal.tsx
@@ -3,21 +3,24 @@ import { absolutePathForPhoto, pathForPhoto } from '@/site/paths';
import { Photo } from '.';
import ShareModal from '@/components/ShareModal';
import { Camera } from '@/camera';
+import { FilmSimulation } from '@/simulation';
export default function PhotoShareModal({
photo,
tag,
camera,
+ simulation,
}: {
photo: Photo
tag?: string
camera?: Camera
+ simulation?: FilmSimulation
}) {
return (
diff --git a/src/photo/PhotoSmall.tsx b/src/photo/PhotoSmall.tsx
index be23c302..5abb304e 100644
--- a/src/photo/PhotoSmall.tsx
+++ b/src/photo/PhotoSmall.tsx
@@ -4,21 +4,24 @@ import Link from 'next/link';
import { cc } from '@/utility/css';
import { pathForPhoto } from '@/site/paths';
import { Camera } from '@/camera';
+import { FilmSimulation } from '@/simulation';
export default function PhotoSmall({
photo,
tag,
camera,
+ simulation,
selected,
}: {
photo: Photo
tag?: string
camera?: Camera
+ simulation?: FilmSimulation
selected?: boolean
}) {
return (
;
@@ -95,7 +95,7 @@ export const convertPhotoToFormData = (
export const convertExifToFormData = (
data: ExifData,
- fujifilmSimulation?: FujifilmSimulation,
+ filmSimulation?: FilmSimulation,
): Record => ({
aspectRatio: (
(data.imageSize?.width ?? 3.0) /
@@ -111,7 +111,7 @@ export const convertExifToFormData = (
exposureCompensation: data.tags?.ExposureCompensation?.toString(),
latitude: data.tags?.GPSLatitude?.toString(),
longitude: data.tags?.GPSLongitude?.toString(),
- filmSimulation: fujifilmSimulation,
+ filmSimulation,
takenAt: data.tags?.DateTimeOriginal
? convertTimestampWithOffsetToPostgresString(
data.tags?.DateTimeOriginal,
@@ -144,7 +144,7 @@ export const convertFormDataToPhotoDbInsert = (
});
return {
- ...(photoForm as PhotoFormData & { filmSimulation?: FujifilmSimulation }),
+ ...(photoForm as PhotoFormData & { filmSimulation?: FilmSimulation }),
...(generateId && !photoForm.id) && { id: generateNanoid() },
// Convert form strings to arrays
tags: convertStringToArray(photoForm.tags),
diff --git a/src/photo/image-response/FilmSimulationImageResponse.tsx b/src/photo/image-response/FilmSimulationImageResponse.tsx
new file mode 100644
index 00000000..437c6e21
--- /dev/null
+++ b/src/photo/image-response/FilmSimulationImageResponse.tsx
@@ -0,0 +1,50 @@
+import { Photo } from '..';
+import ImageCaption from './components/ImageCaption';
+import ImagePhotoGrid from './components/ImagePhotoGrid';
+import ImageContainer from './components/ImageContainer';
+import {
+ labelForFilmSimulation,
+} from '@/vendors/fujifilm';
+import PhotoFilmSimulationIcon from
+ '@/simulation/PhotoFilmSimulationIcon';
+import { FilmSimulation } from '@/simulation';
+
+export default function FilmSimulationImageResponse({
+ simulation,
+ photos,
+ width,
+ height,
+ fontFamily,
+}: {
+ simulation: FilmSimulation,
+ photos: Photo[]
+ width: number
+ height: number
+ fontFamily: string
+}) {
+ return (
+
+
+
+
+
+ {labelForFilmSimulation(simulation).medium}
+
+
+
+ );
+}
diff --git a/src/photo/index.ts b/src/photo/index.ts
index 6f2fe489..6bbb8508 100644
--- a/src/photo/index.ts
+++ b/src/photo/index.ts
@@ -1,3 +1,4 @@
+import { FilmSimulation } from '@/simulation';
import { ABSOLUTE_PATH_FOR_HOME_IMAGE } from '@/site/paths';
import { formatDateFromPostgresString } from '@/utility/date';
import {
@@ -7,7 +8,6 @@ import {
formatExposureTime,
formatFocalLength,
} from '@/utility/exif';
-import { FujifilmSimulation } from '@/vendors/fujifilm';
import camelcaseKeys from 'camelcase-keys';
import type { Metadata } from 'next';
@@ -31,7 +31,7 @@ export interface PhotoExif {
exposureCompensation?: number
latitude?: number
longitude?: number
- filmSimulation?: FujifilmSimulation
+ filmSimulation?: FilmSimulation
takenAt: string
takenAtNaive: string
}
diff --git a/src/photo/server.ts b/src/photo/server.ts
index 5b916114..62d08f33 100644
--- a/src/photo/server.ts
+++ b/src/photo/server.ts
@@ -1,12 +1,12 @@
import { getExtensionFromBlobUrl, getIdFromBlobUrl } from '@/services/blob';
import { convertExifToFormData } from '@/photo/form';
import {
- FujifilmSimulation,
getFujifilmSimulationFromMakerNote,
isExifForFujifilm,
} from '@/vendors/fujifilm';
import { ExifData, ExifParserFactory } from 'ts-exif-parser';
import { PhotoFormData } from './form';
+import { FilmSimulation } from '@/simulation';
export const extractExifDataFromBlobPath = async (
blobPath: string
@@ -26,7 +26,7 @@ export const extractExifDataFromBlobPath = async (
: undefined;
let exifData: ExifData | undefined;
- let filmSimulation: FujifilmSimulation | undefined;
+ let filmSimulation: FilmSimulation | undefined;
if (fileBytes) {
const parser = ExifParserFactory.create(Buffer.from(fileBytes));
diff --git a/src/services/postgres.ts b/src/services/postgres.ts
index 17d430aa..03cd5700 100644
--- a/src/services/postgres.ts
+++ b/src/services/postgres.ts
@@ -10,7 +10,7 @@ import {
import { Camera, Cameras, createCameraKey } from '@/camera';
import { parameterize } from '@/utility/string';
import { Tags } from '@/tag';
-import { FujifilmSimulation, FujifilmSimulations } from '@/vendors/fujifilm';
+import { FilmSimulation, FilmSimulations } from '@/simulation';
const PHOTO_DEFAULT_LIMIT = 100;
@@ -221,7 +221,7 @@ const sqlGetPhotosByCamera = async (
const sqlGetPhotosBySimulation = async (
limit = PHOTO_DEFAULT_LIMIT,
- simulation: FujifilmSimulation,
+ simulation: FilmSimulation,
) => sql`
SELECT * FROM photos
WHERE film_simulation=${simulation}
@@ -281,7 +281,7 @@ const sqlGetPhotosCameraCount = async (camera: Camera) => sql`
`.then(({ rows }) => parseInt(rows[0].count, 10));
const sqlGetPhotosFilmSimulationCount = async (
- simulation: FujifilmSimulation,
+ simulation: FilmSimulation,
) => sql`
SELECT COUNT(*) FROM photos
WHERE film_simulation=${simulation} AND
@@ -305,7 +305,7 @@ const sqlGetPhotosCameraDateRange = async (camera: Camera) => sql`
`.then(({ rows }) => rows[0] as PhotoDateRange);
const sqlGetPhotosFilmSimulationDateRange = async (
- simulation: FujifilmSimulation,
+ simulation: FilmSimulation,
) => sql`
SELECT MIN(taken_at_naive) as start, MAX(taken_at_naive) as end
FROM photos
@@ -352,9 +352,9 @@ const sqlGetUniqueFilmSimulations = async () => sql`
WHERE hidden IS NOT TRUE AND film_simulation IS NOT NULL
GROUP BY film_simulation
ORDER BY film_simulation DESC
-`.then(({ rows }): FujifilmSimulations => rows
+`.then(({ rows }): FilmSimulations => rows
.map(({ film_simulation, count }) => ({
- simulation: film_simulation as FujifilmSimulation,
+ simulation: film_simulation as FilmSimulation,
count: parseInt(count, 10),
})));
@@ -363,7 +363,7 @@ export type GetPhotosOptions = {
limit?: number
tag?: string
camera?: Camera
- simulation?: FujifilmSimulation
+ simulation?: FilmSimulation
takenBefore?: Date
takenAfterInclusive?: Date
includeHidden?: boolean
@@ -469,7 +469,7 @@ export const getPhotosCameraCount = (camera: Camera) =>
export const getUniqueFilmSimulations = () =>
safelyQueryPhotos(sqlGetUniqueFilmSimulations);
export const getPhotosFilmSimulationDateRange =
- (simulation: FujifilmSimulation) => safelyQueryPhotos(() =>
+ (simulation: FilmSimulation) => safelyQueryPhotos(() =>
sqlGetPhotosFilmSimulationDateRange(simulation));
-export const getPhotosFilmSimulationCount = (simulation: FujifilmSimulation) =>
+export const getPhotosFilmSimulationCount = (simulation: FilmSimulation) =>
safelyQueryPhotos(() => sqlGetPhotosFilmSimulationCount(simulation));
diff --git a/src/simulation/FilmSimulationHeader.tsx b/src/simulation/FilmSimulationHeader.tsx
new file mode 100644
index 00000000..6e895b96
--- /dev/null
+++ b/src/simulation/FilmSimulationHeader.tsx
@@ -0,0 +1,40 @@
+import { Photo, PhotoDateRange } from '@/photo';
+import { FilmSimulation, descriptionForFilmSimulationPhotos } from '.';
+import { pathForFilmSimulationShare } from '@/site/paths';
+import PhotoHeader from '@/photo/PhotoHeader';
+import AnimateItems from '@/components/AnimateItems';
+import PhotoFilmSimulation from
+ '@/simulation/PhotoFilmSimulation';
+
+export default function FilmSimulationHeader({
+ simulation,
+ photos,
+ selectedPhoto,
+ count,
+ dateRange,
+}: {
+ simulation: FilmSimulation
+ photos: Photo[]
+ selectedPhoto?: Photo
+ count?: number
+ dateRange?: PhotoDateRange
+}) {
+ return (
+ }
+ entityVerb="Photo"
+ entityDescription={descriptionForFilmSimulationPhotos(
+ photos, undefined, count, dateRange)}
+ photos={photos}
+ selectedPhoto={selectedPhoto}
+ sharePath={pathForFilmSimulationShare(simulation)}
+ count={count}
+ dateRange={dateRange}
+ />]}
+ />
+ );
+}
diff --git a/src/simulation/FilmSimulationOGTile.tsx b/src/simulation/FilmSimulationOGTile.tsx
new file mode 100644
index 00000000..1233a3e4
--- /dev/null
+++ b/src/simulation/FilmSimulationOGTile.tsx
@@ -0,0 +1,50 @@
+import { Photo, PhotoDateRange } from '@/photo';
+import {
+ absolutePathForFilmSimulationImage,
+ pathForFilmSimulation,
+} from '@/site/paths';
+import OGTile from '@/components/OGTile';
+import {
+ FilmSimulation,
+ descriptionForFilmSimulationPhotos,
+ titleForFilmSimulation,
+} from '.';
+
+export type OGLoadingState = 'unloaded' | 'loading' | 'loaded' | 'failed';
+
+export default function FilmSimulationOGTile({
+ simulation,
+ photos,
+ loadingState: loadingStateExternal,
+ riseOnHover,
+ onLoad,
+ onFail,
+ retryTime,
+ count,
+ dateRange,
+}: {
+ simulation: FilmSimulation
+ photos: Photo[]
+ loadingState?: OGLoadingState
+ onLoad?: () => void
+ onFail?: () => void
+ riseOnHover?: boolean
+ retryTime?: number
+ count?: number
+ dateRange?: PhotoDateRange
+}) {
+ return (
+
+ );
+};
diff --git a/src/simulation/FilmSimulationOverview.tsx b/src/simulation/FilmSimulationOverview.tsx
new file mode 100644
index 00000000..acb32584
--- /dev/null
+++ b/src/simulation/FilmSimulationOverview.tsx
@@ -0,0 +1,42 @@
+import { Photo, PhotoDateRange } from '@/photo';
+import SiteGrid from '@/components/SiteGrid';
+import AnimateItems from '@/components/AnimateItems';
+import PhotoGrid from '@/photo/PhotoGrid';
+import FilmSimulationHeader from './FilmSimulationHeader';
+import { FilmSimulation } from '.';
+
+export default function FilmSimulationOverview({
+ simulation,
+ photos,
+ count,
+ dateRange,
+ showMorePath,
+ animateOnFirstLoadOnly,
+}: {
+ simulation: FilmSimulation,
+ photos: Photo[],
+ count: number,
+ dateRange: PhotoDateRange,
+ showMorePath?: string,
+ animateOnFirstLoadOnly?: boolean,
+}) {
+ return (
+
+ ,
+ ]}
+ animateOnFirstLoadOnly
+ />
+
+ }
+ />
+ );
+}
diff --git a/src/simulation/FilmSimulationShareModal.tsx b/src/simulation/FilmSimulationShareModal.tsx
new file mode 100644
index 00000000..3369b82e
--- /dev/null
+++ b/src/simulation/FilmSimulationShareModal.tsx
@@ -0,0 +1,30 @@
+import {
+ absolutePathForFilmSimulation,
+ pathForFilmSimulation,
+} from '@/site/paths';
+import { Photo, PhotoDateRange } from '../photo';
+import ShareModal from '@/components/ShareModal';
+import FilmSimulationOGTile from './FilmSimulationOGTile';
+import { FilmSimulation } from '.';
+
+export default function FilmSimulationShareModal({
+ simulation,
+ photos,
+ count,
+ dateRange,
+}: {
+ simulation: FilmSimulation
+ photos: Photo[]
+ count?: number
+ dateRange?: PhotoDateRange
+}) {
+ return (
+
+
+
+ );
+};
diff --git a/src/simulation/PhotoFilmSimulation.tsx b/src/simulation/PhotoFilmSimulation.tsx
new file mode 100644
index 00000000..61ff6eb7
--- /dev/null
+++ b/src/simulation/PhotoFilmSimulation.tsx
@@ -0,0 +1,58 @@
+import { cc } from '@/utility/css';
+import { labelForFilmSimulation } from '@/vendors/fujifilm';
+import PhotoFilmSimulationIcon from './PhotoFilmSimulationIcon';
+import Badge from '@/components/Badge';
+import Link from 'next/link';
+import { pathForFilmSimulation } from '@/site/paths';
+import { FilmSimulation } from '.';
+
+export default function PhotoFilmSimulation({
+ simulation,
+ type = 'icon-last',
+ badged = true,
+ countOnHover,
+}: {
+ simulation: FilmSimulation
+ type?: 'icon-last' | 'icon-first' | 'icon-only' | 'text-only'
+ badged?: boolean
+ countOnHover?: number
+}) {
+ const { small, medium, large } = labelForFilmSimulation(simulation);
+
+ const renderContent = () => <>
+
+ {small}
+
+
+ {medium}
+
+ >;
+
+ return (
+
+
+ {type !== 'icon-only' && <>
+ {badged
+ ?
+ {renderContent()}
+
+ : {renderContent()}}
+ >}
+ {type !== 'text-only' &&
+
+ }
+
+ {countOnHover !== undefined &&
+
+ {countOnHover}
+ }
+
+ );
+}
diff --git a/src/simulation/PhotoFilmSimulationIcon.tsx b/src/simulation/PhotoFilmSimulationIcon.tsx
new file mode 100644
index 00000000..35988499
--- /dev/null
+++ b/src/simulation/PhotoFilmSimulationIcon.tsx
@@ -0,0 +1,157 @@
+/* eslint-disable max-len */
+import { labelForFilmSimulation } from '@/vendors/fujifilm';
+import { CSSProperties } from 'react';
+import { FilmSimulation } from '.';
+
+const INTRINSIC_WIDTH = 28;
+const INTRINSIC_HEIGHT = 16;
+
+export default function PhotoFilmSimulationIcon({
+ simulation,
+ height = INTRINSIC_HEIGHT,
+ className,
+ style,
+}: {
+ simulation?: FilmSimulation
+ height?: number
+ className?: string
+ style?: CSSProperties
+}) {
+ return (
+
+ );
+}
diff --git a/src/simulation/data.ts b/src/simulation/data.ts
new file mode 100644
index 00000000..a672c24b
--- /dev/null
+++ b/src/simulation/data.ts
@@ -0,0 +1,53 @@
+import {
+ getPhotosCached,
+ getPhotosFilmSimulationCountCached,
+ getPhotosFilmSimulationDateRangeCached,
+} from '@/cache';
+import {
+ PaginationSearchParams,
+ getPaginationForSearchParams,
+} from '@/site/pagination';
+import { pathForFilmSimulation } from '@/site/paths';
+import { FilmSimulation } from '.';
+
+export const getPhotosFilmSimulationDataCached = ({
+ simulation,
+ limit,
+}: {
+ simulation: FilmSimulation,
+ limit?: number,
+}) =>
+ Promise.all([
+ getPhotosCached({ simulation, limit }),
+ getPhotosFilmSimulationCountCached(simulation),
+ getPhotosFilmSimulationDateRangeCached(simulation),
+ ]);
+
+export const getPhotosFilmSimulationDataCachedWithPagination = async ({
+ simulation,
+ limit: limitProp,
+ searchParams,
+}: {
+ simulation: FilmSimulation,
+ limit?: number,
+ searchParams?: PaginationSearchParams,
+}) => {
+ const { offset, limit } = getPaginationForSearchParams(searchParams);
+
+ const [photos, count, dateRange] =
+ await getPhotosFilmSimulationDataCached({
+ simulation,
+ limit: limitProp ?? limit,
+ });
+
+ const showMorePath = count > photos.length
+ ? pathForFilmSimulation(simulation, offset + 1)
+ : undefined;
+
+ return {
+ photos,
+ count,
+ dateRange,
+ showMorePath,
+ };
+};
diff --git a/src/simulation/index.ts b/src/simulation/index.ts
new file mode 100644
index 00000000..05559885
--- /dev/null
+++ b/src/simulation/index.ts
@@ -0,0 +1,61 @@
+import {
+ Photo,
+ PhotoDateRange,
+ descriptionForPhotoSet,
+ photoQuantityText,
+} from '@/photo';
+import {
+ absolutePathForFilmSimulation,
+ absolutePathForFilmSimulationImage,
+} from '@/site/paths';
+import {
+ FujifilmSimulation,
+ labelForFilmSimulation,
+} from '@/vendors/fujifilm';
+
+export type FilmSimulation = FujifilmSimulation;
+
+export type FilmSimulations = {
+ simulation: FilmSimulation
+ count: number
+}[]
+
+export const titleForFilmSimulation = (
+ simulation: FilmSimulation,
+ photos:Photo[],
+ explicitCount?: number,
+) => [
+ labelForFilmSimulation(simulation).large,
+ photoQuantityText(explicitCount ?? photos.length),
+].join(' ');
+
+export const descriptionForFilmSimulationPhotos = (
+ photos: Photo[],
+ dateBased?: boolean,
+ explicitCount?: number,
+ explicitDateRange?: PhotoDateRange,
+) =>
+ descriptionForPhotoSet(
+ photos,
+ undefined,
+ dateBased,
+ explicitCount,
+ explicitDateRange,
+ );
+
+export const generateMetaForFilmSimulation = (
+ simulation: FilmSimulation,
+ photos: Photo[],
+ explicitCount?: number,
+ explicitDateRange?: PhotoDateRange,
+) => ({
+ url: absolutePathForFilmSimulation(simulation),
+ title: titleForFilmSimulation(simulation, photos, explicitCount),
+ description: descriptionForFilmSimulationPhotos(
+ photos,
+ true,
+ explicitCount,
+ explicitDateRange,
+ ),
+ images: absolutePathForFilmSimulationImage(simulation),
+});
diff --git a/src/site/globals.css b/src/site/globals.css
index fedb0261..0b7ef0e6 100644
--- a/src/site/globals.css
+++ b/src/site/globals.css
@@ -40,8 +40,8 @@
/* Required for readonly behavior on */
.disabled-select {
@apply
- bg-gray-100
- dark:bg-gray-900 dark:text-gray-400
+ text-medium
+ bg-gray-100 dark:bg-gray-900
pointer-events-none
}
input[type=file] {
diff --git a/src/site/paths.ts b/src/site/paths.ts
index 8528c4ce..cc9a0bb9 100644
--- a/src/site/paths.ts
+++ b/src/site/paths.ts
@@ -1,3 +1,4 @@
+/* eslint-disable max-len */
import { Photo } from '@/photo';
import { BASE_URL } from './config';
import {
@@ -5,6 +6,7 @@ import {
createCameraKey,
getCameraFromKey,
} from '@/camera';
+import { FilmSimulation } from '@/simulation';
// Core paths
export const PATH_ROOT = '/';
@@ -14,9 +16,10 @@ export const PATH_SIGN_IN = '/sign-in';
export const PATH_OG = '/og';
// Path prefixes
-export const PREFIX_PHOTO = '/p';
-export const PREFIX_TAG = '/tag';
-export const PREFIX_CAMERA = '/shot-on';
+export const PREFIX_PHOTO = '/p';
+export const PREFIX_TAG = '/tag';
+export const PREFIX_CAMERA = '/shot-on';
+export const PREFIX_FILM_SIMULATION = '/film';
// Dynamic paths
const PATH_PHOTO_DYNAMIC = `${PREFIX_PHOTO}/:photoId`;
@@ -92,19 +95,23 @@ export const pathForPhoto = (
photo: PhotoOrPhotoId,
tag?: string,
camera?: Camera,
+ simulation?: FilmSimulation,
) =>
tag
? `${pathForTag(tag)}/${getPhotoId(photo)}`
: camera
? `${pathForCamera(camera)}/${getPhotoId(photo)}`
- : `${PREFIX_PHOTO}/${getPhotoId(photo)}`;
+ : simulation
+ ? `${pathForFilmSimulation(simulation)}/${getPhotoId(photo)}`
+ : `${PREFIX_PHOTO}/${getPhotoId(photo)}`;
export const pathForPhotoShare = (
photo: PhotoOrPhotoId,
tag?: string,
camera?: Camera,
+ simulation?: FilmSimulation,
) =>
- `${pathForPhoto(photo, tag, camera)}/${SHARE}`;
+ `${pathForPhoto(photo, tag, camera, simulation)}/${SHARE}`;
export const pathForTag = (tag: string, next?: number) =>
pathWithNext(
@@ -121,12 +128,23 @@ export const pathForCamera = (camera: Camera, next?: number) =>
export const pathForCameraShare = (camera: Camera) =>
`${pathForCamera(camera)}/${SHARE}`;
+export const pathForFilmSimulation =
+ (simulation: FilmSimulation, next?: number) =>
+ pathWithNext(
+ `${PREFIX_FILM_SIMULATION}/${simulation}`,
+ next,
+ );
+
+export const pathForFilmSimulationShare = (simulation: FilmSimulation) =>
+ `${pathForFilmSimulation(simulation)}/${SHARE}`;
+
export const absolutePathForPhoto = (
photo: PhotoOrPhotoId,
tag?: string,
camera?: Camera,
+ simulation?: FilmSimulation
) =>
- `${BASE_URL}${pathForPhoto(photo, tag, camera)}`;
+ `${BASE_URL}${pathForPhoto(photo, tag, camera, simulation)}`;
export const absolutePathForTag = (tag: string) =>
`${BASE_URL}${pathForTag(tag)}`;
@@ -134,6 +152,9 @@ export const absolutePathForTag = (tag: string) =>
export const absolutePathForCamera= (camera: Camera) =>
`${BASE_URL}${pathForCamera(camera)}`;
+export const absolutePathForFilmSimulation = (simulation: FilmSimulation) =>
+ `${BASE_URL}${pathForFilmSimulation(simulation)}`;
+
export const absolutePathForPhotoImage = (photo: PhotoOrPhotoId) =>
`${absolutePathForPhoto(photo)}/image`;
@@ -143,6 +164,9 @@ export const absolutePathForTagImage = (tag: string) =>
export const absolutePathForCameraImage= (camera: Camera) =>
`${absolutePathForCamera(camera)}/image`;
+export const absolutePathForFilmSimulationImage = (simulation: FilmSimulation) =>
+ `${absolutePathForFilmSimulation(simulation)}/image`;
+
// p/[photoId]
export const isPathPhoto = (pathname = '') =>
new RegExp(`^${PREFIX_PHOTO}/[^/]+/?$`).test(pathname);
@@ -183,6 +207,23 @@ export const isPathCameraPhoto = (pathname = '') =>
export const isPathCameraPhotoShare = (pathname = '') =>
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/${SHARE}/?$`).test(pathname);
+// film/[simulation]
+export const isPathFilmSimulation = (pathname = '') =>
+ new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/?$`).test(pathname);
+
+// film/[simulation]/share
+export const isPathFilmSimulationShare = (pathname = '') =>
+ new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/${SHARE}/?$`).test(pathname);
+
+// film/[simulation]/[photoId]
+export const isPathFilmSimulationPhoto = (pathname = '') =>
+ new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/[^/]+/?$`).test(pathname);
+
+// film/[simulation]/[photoId]/share
+export const isPathFilmSimulationPhotoShare = (pathname = '') =>
+ new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/[^/]+/${SHARE}/?$`)
+ .test(pathname);
+
export const checkPathPrefix = (pathname = '', prefix: string) =>
pathname.toLowerCase().startsWith(prefix);
@@ -205,6 +246,7 @@ export const getPathComponents = (pathname = ''): {
photoId?: string
tag?: string
camera?: Camera
+ simulation?: FilmSimulation
} => {
const photoIdFromPhoto = pathname.match(
new RegExp(`^${PREFIX_PHOTO}/([^/]+)`))?.[1];
@@ -212,10 +254,14 @@ export const getPathComponents = (pathname = ''): {
new RegExp(`^${PREFIX_TAG}/[^/]+/((?!${SHARE})[^/]+)`))?.[1];
const photoIdFromCamera = pathname.match(
new RegExp(`^${PREFIX_CAMERA}/[^/]+/((?!${SHARE})[^/]+)`))?.[1];
+ const photoIdFromFilmSimulation = pathname.match(
+ new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/((?!${SHARE})[^/]+)`))?.[1];
const tag = pathname.match(
new RegExp(`^${PREFIX_TAG}/([^/]+)`))?.[1];
const cameraString = pathname.match(
new RegExp(`^${PREFIX_CAMERA}/([^/]+)`))?.[1];
+ const simulation = pathname.match(
+ new RegExp(`^${PREFIX_FILM_SIMULATION}/([^/]+)`))?.[1] as FilmSimulation;
const camera = cameraString
? getCameraFromKey(cameraString)
@@ -225,25 +271,30 @@ export const getPathComponents = (pathname = ''): {
photoId: (
photoIdFromPhoto ||
photoIdFromTag ||
- photoIdFromCamera
+ photoIdFromCamera ||
+ photoIdFromFilmSimulation
),
tag,
camera,
+ simulation,
};
};
export const getEscapePath = (pathname?: string) => {
- const { photoId, tag, camera } = getPathComponents(pathname);
+ const { photoId, tag, camera, simulation } = getPathComponents(pathname);
if (
(photoId && isPathPhoto(pathname)) ||
(tag && isPathTag(pathname)) ||
- (camera && isPathCamera(pathname))
+ (camera && isPathCamera(pathname)) ||
+ (simulation && isPathFilmSimulation(pathname))
) {
return PATH_GRID;
} else if (photoId && isPathTagPhotoShare(pathname)) {
return pathForPhoto(photoId, tag);
} else if (photoId && isPathCameraPhotoShare(pathname)) {
return pathForPhoto(photoId, undefined, camera);
+ } else if (photoId && isPathFilmSimulationPhotoShare(pathname)) {
+ return pathForPhoto(photoId, undefined, undefined, simulation);
} else if (photoId && isPathPhotoShare(pathname)) {
return pathForPhoto(photoId);
} else if (tag && (
@@ -256,5 +307,10 @@ export const getEscapePath = (pathname?: string) => {
isPathCameraShare(pathname)
)) {
return pathForCamera(camera);
+ } else if (simulation && (
+ isPathFilmSimulationPhoto(pathname) ||
+ isPathFilmSimulationShare(pathname)
+ )) {
+ return pathForFilmSimulation(simulation);
}
};
diff --git a/src/vendors/fujifilm/PhotoFujifilmSimulation.tsx b/src/vendors/fujifilm/PhotoFujifilmSimulation.tsx
deleted file mode 100644
index f946b4af..00000000
--- a/src/vendors/fujifilm/PhotoFujifilmSimulation.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { cc } from '@/utility/css';
-import {
- FujifilmSimulation,
- getLabelForFilmSimulation,
-} from '@/vendors/fujifilm';
-import PhotoFujifilmSimulationIcon from './PhotoFujifilmSimulationIcon';
-import Badge from '@/components/Badge';
-
-export default function PhotoFujifilmSimulation({
- simulation,
- type = 'icon-last',
- badged = true,
-}: {
- simulation: FujifilmSimulation
- type?: 'icon-last' | 'icon-first' | 'icon-only' | 'text-only'
- badged?: boolean
-}) {
- const { small, medium, large } = getLabelForFilmSimulation(simulation);
-
- const renderContent = () => <>
-
- {small}
-
-
- {medium}
-
- >;
-
- return (
-
- {type !== 'icon-only' && <>
- {badged
- ? {renderContent()}
- : {renderContent()}}
- >}
- {type !== 'text-only' &&
-
- }
-
- );
-}
diff --git a/src/vendors/fujifilm/PhotoFujifilmSimulationIcon.tsx b/src/vendors/fujifilm/PhotoFujifilmSimulationIcon.tsx
deleted file mode 100644
index ddcc4f08..00000000
--- a/src/vendors/fujifilm/PhotoFujifilmSimulationIcon.tsx
+++ /dev/null
@@ -1,150 +0,0 @@
-/* eslint-disable max-len */
-import {
- FujifilmSimulation,
- getLabelForFilmSimulation,
-} from '@/vendors/fujifilm';
-
-export default function PhotoFujifilmSimulationIcon({
- simulation,
- className,
-}: {
- simulation?: FujifilmSimulation;
- className?: string
-}) {
- const contentForSimulation = (): JSX.Element => {
- switch (simulation) {
- case 'monochrome': return <>
-
-
- >;
- case 'monochrome-ye': return <>
-
-
-
- >;
- case 'monochrome-r': return <>
-
-
-
- >;
- case 'monochrome-g': return <>
-
-
-
- >;
- case 'sepia': return <>
-
-
-
- >;
- case 'acros': return <>
-
-
- >;
- case 'acros-ye': return <>
-
-
-
- >;
- case 'acros-r': return <>
-
-
-
- >;
- case 'acros-g': return <>
-
-
-
- >;
- case 'provia': return <>
-
-
-
- >;
- case 'portrait': return <>
-
-
- >;
- case 'portrait-saturation': return <>
-
-
-
- >;
- case 'portrait-skin-tone': return <>
-
-
- >;
- case 'portrait-sharpness': return <>
-
-
-
- >;
- case 'portrait-ex': return <>
-
-
-
- >;
- case 'velvia': return <>
-
-
- >;
- case 'pro-neg-std': return <>
-
-
-
- >;
- case 'pro-neg-hi': return <>
-
-
-
- >;
- case 'classic-chrome': return <>
-
-
-
- >;
- case 'eterna': return <>
-
-
- >;
- case 'classic-neg': return <>
-
-
-
- >;
- case 'eterna-bleach-bypass': return <>
-
-
-
- >;
- case 'nostalgic-neg': return <>
-
-
-
- >;
- case 'reala': return <>
-
-
- >;
- default: return <>
-
- >;
- }
- };
-
- return (
-
- );
-}
diff --git a/src/vendors/fujifilm/index.ts b/src/vendors/fujifilm/index.ts
index 4aa9c41e..0707e2a2 100644
--- a/src/vendors/fujifilm/index.ts
+++ b/src/vendors/fujifilm/index.ts
@@ -46,11 +46,6 @@ export type FujifilmSimulation =
FujifilmSimulationFromSaturation |
FujifilmMode;
-export type FujifilmSimulations = {
- simulation: FujifilmSimulation
- count: number
-}[]
-
export const isExifForFujifilm = (data: ExifData) =>
data.tags?.Make === MAKE_FUJIFILM;
@@ -233,9 +228,7 @@ export const FILM_SIMULATION_FORM_INPUT_OPTIONS = Object
))
.sort((a, b) => a.label.localeCompare(b.label));
-export const getLabelForFilmSimulation = (
- simulation: FujifilmSimulation
-) =>
+export const labelForFilmSimulation = (simulation: FujifilmSimulation) =>
FILM_SIMULATION_LABELS[simulation];
const parseFujifilmMakerNote = (