From f728e3981b189c3746d17ebc722b23c0576a0f2a Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 6 Nov 2023 10:05:20 -0600 Subject: [PATCH] Build out all film simulation pages --- __tests__/path.test.ts | 74 +++++++-- .../{film => film-demo}/animate/page.tsx | 6 +- src/app/(static)/{film => film-demo}/page.tsx | 6 +- .../film/[simulation]/[photoId]/layout.tsx | 85 ++++++++++ .../film/[simulation]/[photoId]/page.tsx | 3 + .../[simulation]/[photoId]/share/page.tsx | 34 ++++ .../film/[simulation]/image/route.tsx | 43 +++++ src/app/(static)/film/[simulation]/page.tsx | 76 +++++++++ .../(static)/film/[simulation]/share/page.tsx | 75 +++++++++ .../tag/[tag]/[photoId]/share/page.tsx | 2 +- src/app/(static)/tag/[tag]/image/route.tsx | 2 +- src/cache/index.ts | 6 +- src/components/Badge.tsx | 4 + src/photo/PhotoDetailPage.tsx | 17 ++ src/photo/PhotoGrid.tsx | 4 + src/photo/PhotoGridSidebar.tsx | 20 +-- src/photo/PhotoLarge.tsx | 14 +- src/photo/PhotoShareModal.tsx | 7 +- src/photo/PhotoSmall.tsx | 5 +- src/photo/form.ts | 8 +- .../FilmSimulationImageResponse.tsx | 50 ++++++ src/photo/index.ts | 4 +- src/photo/server.ts | 4 +- src/services/postgres.ts | 18 +- src/simulation/FilmSimulationHeader.tsx | 40 +++++ src/simulation/FilmSimulationOGTile.tsx | 50 ++++++ src/simulation/FilmSimulationOverview.tsx | 42 +++++ src/simulation/FilmSimulationShareModal.tsx | 30 ++++ src/simulation/PhotoFilmSimulation.tsx | 58 +++++++ src/simulation/PhotoFilmSimulationIcon.tsx | 157 ++++++++++++++++++ src/simulation/data.ts | 53 ++++++ src/simulation/index.ts | 61 +++++++ src/site/globals.css | 4 +- src/site/paths.ts | 74 ++++++++- .../fujifilm/PhotoFujifilmSimulation.tsx | 47 ------ .../fujifilm/PhotoFujifilmSimulationIcon.tsx | 150 ----------------- src/vendors/fujifilm/index.ts | 9 +- 37 files changed, 1061 insertions(+), 281 deletions(-) rename src/app/(static)/{film => film-demo}/animate/page.tsx (92%) rename src/app/(static)/{film => film-demo}/page.tsx (74%) create mode 100644 src/app/(static)/film/[simulation]/[photoId]/layout.tsx create mode 100644 src/app/(static)/film/[simulation]/[photoId]/page.tsx create mode 100644 src/app/(static)/film/[simulation]/[photoId]/share/page.tsx create mode 100644 src/app/(static)/film/[simulation]/image/route.tsx create mode 100644 src/app/(static)/film/[simulation]/page.tsx create mode 100644 src/app/(static)/film/[simulation]/share/page.tsx create mode 100644 src/photo/image-response/FilmSimulationImageResponse.tsx create mode 100644 src/simulation/FilmSimulationHeader.tsx create mode 100644 src/simulation/FilmSimulationOGTile.tsx create mode 100644 src/simulation/FilmSimulationOverview.tsx create mode 100644 src/simulation/FilmSimulationShareModal.tsx create mode 100644 src/simulation/PhotoFilmSimulation.tsx create mode 100644 src/simulation/PhotoFilmSimulationIcon.tsx create mode 100644 src/simulation/data.ts create mode 100644 src/simulation/index.ts delete mode 100644 src/vendors/fujifilm/PhotoFujifilmSimulation.tsx delete mode 100644 src/vendors/fujifilm/PhotoFujifilmSimulationIcon.tsx 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 ( + + {(() => { + // Self-calling switch function and non-fragment groups + // necessary for ImageResponse compatibility + 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 + + ; + } + })()} + + ); +} 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