From 503ef6ca7cf1aaaffe218bdfc2e5fc065a5c20d6 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 5 Nov 2023 12:42:00 -0600 Subject: [PATCH 1/6] Add fujifilm simulations to /grid sidebar --- README.md | 9 ++-- src/app/(static)/film/animate/page.tsx | 2 +- src/app/(static)/film/page.tsx | 2 +- src/app/(static)/grid/page.tsx | 6 ++- src/cache/index.ts | 11 +++++ src/components/HeaderList.tsx | 3 ++ src/photo/PhotoGridSidebar.tsx | 24 +++++++++++ src/services/postgres.ts | 17 ++++++++ src/site/SiteChecklistClient.tsx | 42 ++++++++++--------- src/site/config.ts | 7 +++- .../fujifilm/PhotoFujifilmSimulation.tsx | 20 +++++---- .../fujifilm/PhotoFujifilmSimulationIcon.tsx | 12 +++++- src/vendors/fujifilm/index.ts | 5 +++ 13 files changed, 121 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index a0f985d3..7597c2aa 100644 --- a/README.md +++ b/README.md @@ -62,10 +62,11 @@ Installation ### 6. Optional configuration -- Set `NEXT_PUBLIC_HIDE_REPO_LINK = 1` to remove footer link to repo -- Set `NEXT_PUBLIC_PRO_MODE = 1` to enable higher quality image storage -- Set `NEXT_PUBLIC_PUBLIC_API = 1` to enable a public API available at `/api` -- Set `NEXT_PUBLIC_OG_TEXT_ALIGNMENT = BOTTOM` to keep OG image text bottom aligned (default is top) +- `NEXT_PUBLIC_PRO_MODE = 1` enables higher quality image storage +- `NEXT_PUBLIC_PUBLIC_API = 1` enables public API available at `/api` +- `NEXT_PUBLIC_OG_TEXT_ALIGNMENT = BOTTOM` keeps OG image text bottom aligned (default is top) +- `NEXT_PUBLIC_HIDE_REPO_LINK = 1` removes footer link to repo +- `NEXT_PUBLIC_HIDE_FILM_SIMULATIONS = 1` prevents Fujifilm simulations showing up in `/grid` sidebar FAQ - diff --git a/src/app/(static)/film/animate/page.tsx b/src/app/(static)/film/animate/page.tsx index c98d4beb..d7c70f97 100644 --- a/src/app/(static)/film/animate/page.tsx +++ b/src/app/(static)/film/animate/page.tsx @@ -28,7 +28,7 @@ export default function FilmPage() {
diff --git a/src/app/(static)/film/page.tsx b/src/app/(static)/film/page.tsx index 31ac78de..8a4b4088 100644 --- a/src/app/(static)/film/page.tsx +++ b/src/app/(static)/film/page.tsx @@ -9,7 +9,7 @@ export default function FilmPage() {
)}
diff --git a/src/app/(static)/grid/page.tsx b/src/app/(static)/grid/page.tsx index 3688c57e..e400c6ba 100644 --- a/src/app/(static)/grid/page.tsx +++ b/src/app/(static)/grid/page.tsx @@ -2,6 +2,7 @@ import { getPhotosCached, getPhotosCountCached, getUniqueCamerasCached, + getUniqueFilmSimulationsCached, getUniqueTagsCached, } from '@/cache'; import SiteGrid from '@/components/SiteGrid'; @@ -16,6 +17,7 @@ import { getPaginationForSearchParams, } from '@/site/pagination'; import PhotoGridSidebar from '@/photo/PhotoGridSidebar'; +import { SHOW_FILM_SIMULATIONS } from '@/site/config'; export const runtime = 'edge'; @@ -32,11 +34,13 @@ export default async function GridPage({ searchParams }: PaginationParams) { photosCount, tags, cameras, + simulations, ] = await Promise.all([ getPhotosCached({ limit }), getPhotosCountCached(), getUniqueTagsCached(), getUniqueCamerasCached(), + SHOW_FILM_SIMULATIONS ? getUniqueFilmSimulationsCached() : [], ]); const showMorePath = photosCount > photos.length @@ -48,7 +52,7 @@ export default async function GridPage({ searchParams }: PaginationParams) { ? } contentSide={
- +
} sideHiddenOnMobile /> diff --git a/src/cache/index.ts b/src/cache/index.ts index ccc85309..45e6bb41 100644 --- a/src/cache/index.ts +++ b/src/cache/index.ts @@ -12,6 +12,7 @@ import { getPhotosTagDateRange, getPhotosCameraDateRange, getUniqueTagsHidden, + getUniqueFilmSimulations, } from '@/services/postgres'; import { parseCachedPhotosDates, parseCachedPhotoDates } from '@/photo'; import { getBlobPhotoUrls, getBlobUploadUrls } from '@/services/blob'; @@ -24,6 +25,7 @@ const KEY_PHOTOS_COUNT = `${KEY_PHOTOS}-count`; const KEY_PHOTOS_DATE_RANGE = `${KEY_PHOTOS}-date-range`; const KEY_TAGS = 'tags'; const KEY_CAMERAS = 'cameras'; +const KEY_FILM_SIMULATIONS = 'film-simulations'; const KEY_BLOB = 'blob'; // Temporary key to clear caches on forked blogs const KEY_NEW_QUERY = 'new-query'; @@ -210,6 +212,15 @@ export const getUniqueCamerasCached: typeof getUniqueCameras = (...args) => } )(); +// eslint-disable-next-line max-len +export const getUniqueFilmSimulationsCached: typeof getUniqueFilmSimulations = (...args) => + unstable_cache( + () => getUniqueFilmSimulations(...args), + [KEY_PHOTOS, KEY_FILM_SIMULATIONS], { + tags: [KEY_PHOTOS, KEY_FILM_SIMULATIONS], + } + )(); + export const getBlobUploadUrlsCached: typeof getBlobUploadUrls = (...args) => unstable_cache( () => getBlobUploadUrls(...args), diff --git a/src/components/HeaderList.tsx b/src/components/HeaderList.tsx index f75bdff7..8a57ef15 100644 --- a/src/components/HeaderList.tsx +++ b/src/components/HeaderList.tsx @@ -4,15 +4,18 @@ import { ReactNode } from 'react'; export default function HeaderList({ title, + className, icon, items, }: { title?: string, + className?: string, icon?: JSX.Element, items: ReactNode[] }) { return ( )} />} + {simulations.length > 0 && } + className="space-y-0.5" + items={simulations.map(({ simulation }) => +
+ +
)} + />} {photosCount > 0 && } diff --git a/src/services/postgres.ts b/src/services/postgres.ts index e666423c..ad409d50 100644 --- a/src/services/postgres.ts +++ b/src/services/postgres.ts @@ -10,6 +10,7 @@ import { import { Camera, Cameras, createCameraKey } from '@/camera'; import { parameterize } from '@/utility/string'; import { Tags } from '@/tag'; +import { FujifilmSimulation, FujifilmSimulations } from '@/vendors/fujifilm'; const PHOTO_DEFAULT_LIMIT = 100; @@ -318,6 +319,18 @@ const sqlGetUniqueCameras = async () => sql` count: parseInt(count, 10), }))); +const sqlGetUniqueFilmSimulations = async () => sql` + SELECT DISTINCT film_simulation, COUNT(*) + FROM photos + WHERE hidden IS NOT TRUE AND film_simulation IS NOT NULL + GROUP BY film_simulation + ORDER BY film_simulation DESC +`.then(({ rows }): FujifilmSimulations => rows + .map(({ film_simulation, count }) => ({ + simulation: film_simulation as FujifilmSimulation, + count: parseInt(count, 10), + }))); + export type GetPhotosOptions = { sortBy?: 'createdAt' | 'takenAt' | 'priority' limit?: number @@ -422,3 +435,7 @@ export const getPhotosCameraDateRange = (camera: Camera) => safelyQueryPhotos(() => sqlGetPhotosCameraDateRange(camera)); export const getPhotosCameraCount = (camera: Camera) => safelyQueryPhotos(() => sqlGetPhotosCameraCount(camera)); + +// FILM SIMULATIONS +export const getUniqueFilmSimulations = () => + safelyQueryPhotos(sqlGetUniqueFilmSimulations); diff --git a/src/site/SiteChecklistClient.tsx b/src/site/SiteChecklistClient.tsx index 2b1b03c1..ee50d69f 100644 --- a/src/site/SiteChecklistClient.tsx +++ b/src/site/SiteChecklistClient.tsx @@ -17,6 +17,7 @@ import IconButton from '@/components/IconButton'; import InfoBlock from '@/components/InfoBlock'; import Checklist from '@/components/Checklist'; import { toastSuccess } from '@/toast'; +import { ConfigChecklistStatus } from './config'; export default function SiteChecklistClient({ hasPostgres, @@ -26,22 +27,13 @@ export default function SiteChecklistClient({ hasTitle, hasDomain, showRepoLink, + showFilmSimulations, isProModeEnabled, isPublicApiEnabled, isOgTextBottomAligned, showRefreshButton, secret, -}: { - hasPostgres: boolean - hasBlob: boolean - hasAuth: boolean - hasAdminUser: boolean - hasTitle: boolean - hasDomain: boolean - showRepoLink: boolean - isProModeEnabled: boolean - isPublicApiEnabled: boolean - isOgTextBottomAligned: boolean +}: ConfigChecklistStatus & { showRefreshButton?: boolean secret: string }) { @@ -210,15 +202,6 @@ export default function SiteChecklistClient({ title="Settings" icon={} > - - Set environment variable to {'"1"'} to hide footer link: - {renderEnvVars(['NEXT_PUBLIC_HIDE_REPO_LINK'])} - + + Set environment variable to {'"1"'} to hide footer link: + {renderEnvVars(['NEXT_PUBLIC_HIDE_REPO_LINK'])} + + + Set environment variable to {'"1"'} to prevent + simulations showing up in /grid sidebar: + {renderEnvVars(['NEXT_PUBLIC_HIDE_FILM_SIMULATIONS'])} + {showRefreshButton &&
diff --git a/src/site/config.ts b/src/site/config.ts index 1a9469db..5fe8d538 100644 --- a/src/site/config.ts +++ b/src/site/config.ts @@ -28,11 +28,13 @@ export const BASE_URL = process.env.NODE_ENV === 'production' ? makeUrlAbsolute(SITE_DOMAIN).toLowerCase() : 'http://localhost:3000'; -export const SHOW_REPO_LINK = process.env.NEXT_PUBLIC_HIDE_REPO_LINK !== '1'; export const PRO_MODE_ENABLED = process.env.NEXT_PUBLIC_PRO_MODE === '1'; export const PUBLIC_API_ENABLED = process.env.NEXT_PUBLIC_PUBLIC_API === '1'; export const OG_TEXT_BOTTOM_ALIGNMENT = (process.env.NEXT_PUBLIC_OG_TEXT_ALIGNMENT ?? '').toUpperCase() === 'BOTTOM'; +export const SHOW_REPO_LINK = process.env.NEXT_PUBLIC_HIDE_REPO_LINK !== '1'; +export const SHOW_FILM_SIMULATIONS = + process.env.NEXT_PUBLIC_HIDE_FILM_SIMULATIONS !== '1'; export const CONFIG_CHECKLIST_STATUS = { hasPostgres: (process.env.POSTGRES_HOST ?? '').length > 0, @@ -45,11 +47,14 @@ export const CONFIG_CHECKLIST_STATUS = { hasTitle: (process.env.NEXT_PUBLIC_SITE_TITLE ?? '').length > 0, hasDomain: (process.env.NEXT_PUBLIC_SITE_DOMAIN ?? '').length > 0, showRepoLink: SHOW_REPO_LINK, + showFilmSimulations: SHOW_FILM_SIMULATIONS, isProModeEnabled: PRO_MODE_ENABLED, isPublicApiEnabled: PUBLIC_API_ENABLED, isOgTextBottomAligned: OG_TEXT_BOTTOM_ALIGNMENT, }; +export type ConfigChecklistStatus = typeof CONFIG_CHECKLIST_STATUS; + export const IS_SITE_READY = CONFIG_CHECKLIST_STATUS.hasPostgres && CONFIG_CHECKLIST_STATUS.hasBlob && diff --git a/src/vendors/fujifilm/PhotoFujifilmSimulation.tsx b/src/vendors/fujifilm/PhotoFujifilmSimulation.tsx index 6d713cf0..f946b4af 100644 --- a/src/vendors/fujifilm/PhotoFujifilmSimulation.tsx +++ b/src/vendors/fujifilm/PhotoFujifilmSimulation.tsx @@ -8,11 +8,11 @@ import Badge from '@/components/Badge'; export default function PhotoFujifilmSimulation({ simulation, - showIconFirst, + type = 'icon-last', badged = true, }: { simulation: FujifilmSimulation - showIconFirst?: boolean + type?: 'icon-last' | 'icon-first' | 'icon-only' | 'text-only' badged?: boolean }) { const { small, medium, large } = getLabelForFilmSimulation(simulation); @@ -31,15 +31,17 @@ export default function PhotoFujifilmSimulation({ title={`Film Simulation: ${large}`} className="inline-flex items-center gap-1" > - {badged - ? {renderContent()} - : {renderContent()}} - + {badged + ? {renderContent()} + : {renderContent()}} + } + {type !== 'text-only' && - - + + } ); } diff --git a/src/vendors/fujifilm/PhotoFujifilmSimulationIcon.tsx b/src/vendors/fujifilm/PhotoFujifilmSimulationIcon.tsx index ebb5977c..ddcc4f08 100644 --- a/src/vendors/fujifilm/PhotoFujifilmSimulationIcon.tsx +++ b/src/vendors/fujifilm/PhotoFujifilmSimulationIcon.tsx @@ -6,8 +6,10 @@ import { export default function PhotoFujifilmSimulationIcon({ simulation, + className, }: { - simulation: FujifilmSimulation; + simulation?: FujifilmSimulation; + className?: string }) { const contentForSimulation = (): JSX.Element => { switch (simulation) { @@ -124,12 +126,18 @@ export default function PhotoFujifilmSimulationIcon({ ; + default: return <> + + ; } }; return ( data.tags?.Make === MAKE_FUJIFILM; From bf5bb1b83a47dd4a55db109ed21f9d9ec43b8b7f Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 5 Nov 2023 20:37:00 -0600 Subject: [PATCH 2/6] Add full film simulation postgres queries --- src/cache/index.ts | 47 +++++++++++++++++++++++++++++------- src/services/postgres.ts | 52 +++++++++++++++++++++++++++++++++------- 2 files changed, 81 insertions(+), 18 deletions(-) diff --git a/src/cache/index.ts b/src/cache/index.ts index 45e6bb41..2da06f8c 100644 --- a/src/cache/index.ts +++ b/src/cache/index.ts @@ -13,12 +13,15 @@ import { getPhotosCameraDateRange, getUniqueTagsHidden, getUniqueFilmSimulations, + getPhotosFilmSimulationDateRange, + getPhotosFilmSimulationCount, } from '@/services/postgres'; import { parseCachedPhotosDates, parseCachedPhotoDates } from '@/photo'; 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'; const KEY_PHOTOS = 'photos'; const KEY_PHOTOS_COUNT = `${KEY_PHOTOS}-count`; @@ -27,8 +30,6 @@ const KEY_TAGS = 'tags'; const KEY_CAMERAS = 'cameras'; const KEY_FILM_SIMULATIONS = 'film-simulations'; const KEY_BLOB = 'blob'; -// Temporary key to clear caches on forked blogs -const KEY_NEW_QUERY = 'new-query'; // eslint-disable-next-line max-len const getPhotosCacheKeyForOption = ( @@ -39,8 +40,8 @@ const getPhotosCacheKeyForOption = ( // Primitive keys case 'sortBy': case 'limit': - case 'offset': case 'tag': + case 'simulation': case 'includeHidden': { const value = options[option]; return value ? `${option}-${value}` : null; @@ -81,12 +82,18 @@ const getPhotoTagCountKey = (tag: string) => const getPhotoCameraCountKey = (camera: Camera) => `${KEY_PHOTOS_COUNT}-${KEY_CAMERAS}-${createCameraKey(camera)}`; +const getPhotoFilmSimulationCountKey = (simulation: FujifilmSimulation) => + `${KEY_PHOTOS_COUNT}-${KEY_FILM_SIMULATIONS}-${simulation}`; + const getPhotoTagDateRangeKey = (tag: string) => `${KEY_PHOTOS_DATE_RANGE}-${KEY_TAGS}-${tag}`; const getPhotoCameraDateRangeKey = (camera: Camera) => `${KEY_PHOTOS_DATE_RANGE}-${KEY_CAMERAS}-${createCameraKey(camera)}`; +const getPhotoFilmSimulationDateRangeKey = (simulation: FujifilmSimulation) => + `${KEY_PHOTOS_DATE_RANGE}-${KEY_FILM_SIMULATIONS}-${simulation}`; + export const revalidatePhotosKey = () => revalidateTag(KEY_PHOTOS); @@ -96,6 +103,9 @@ export const revalidateTagsKey = () => export const revalidateCamerasKey = () => revalidateTag(KEY_CAMERAS); +export const revalidateFilmSimulationsKey = () => + revalidateTag(KEY_FILM_SIMULATIONS); + export const revalidateBlobKey = () => revalidateTag(KEY_BLOB); @@ -108,6 +118,7 @@ export const revalidateAllKeys = () => { revalidatePhotosAndBlobKeys(); revalidateTagsKey(); revalidateCamerasKey(); + revalidateFilmSimulationsKey(); }; export const revalidateAllKeysAndPaths = () => { @@ -161,6 +172,15 @@ export const getPhotosCameraCountCached: typeof getPhotosCameraCount = (...args) } )(); +// eslint-disable-next-line max-len +export const getPhotosFilmSimulationCountCached: typeof getPhotosFilmSimulationCount = (...args) => + unstable_cache( + () => getPhotosFilmSimulationCount(...args), + [KEY_PHOTOS, getPhotoFilmSimulationCountKey(...args)], { + tags: [KEY_PHOTOS, getPhotoFilmSimulationCountKey(...args)], + } + )(); + // eslint-disable-next-line max-len export const getPhotosTagDateRangeCached: typeof getPhotosTagDateRange = (...args) => unstable_cache( @@ -179,6 +199,15 @@ export const getPhotosCameraDateRangeCached: typeof getPhotosCameraDateRange = ( } )(); +// eslint-disable-next-line max-len +export const getPhotosFilmSimulationDateRangeCached: typeof getPhotosFilmSimulationDateRange = (...args) => + unstable_cache( + () => getPhotosFilmSimulationDateRange(...args), + [KEY_PHOTOS, getPhotoFilmSimulationDateRangeKey(...args)], { + tags: [KEY_PHOTOS, getPhotoFilmSimulationDateRangeKey(...args)], + } + )(); + export const getPhotoCached: typeof getPhoto = (...args) => unstable_cache( () => getPhoto(...args), @@ -190,8 +219,8 @@ export const getPhotoCached: typeof getPhoto = (...args) => export const getUniqueTagsCached: typeof getUniqueTags = (...args) => unstable_cache( () => getUniqueTags(...args), - [KEY_PHOTOS, KEY_TAGS, KEY_NEW_QUERY], { - tags: [KEY_PHOTOS, KEY_TAGS, KEY_NEW_QUERY], + [KEY_PHOTOS, KEY_TAGS], { + tags: [KEY_PHOTOS, KEY_TAGS], } )(); @@ -199,16 +228,16 @@ export const getUniqueTagsCached: typeof getUniqueTags = (...args) => export const getUniqueTagsHiddenCached: typeof getUniqueTagsHidden = (...args) => unstable_cache( () => getUniqueTagsHidden(...args), - [KEY_PHOTOS, KEY_TAGS, KEY_NEW_QUERY], { - tags: [KEY_PHOTOS, KEY_TAGS, KEY_NEW_QUERY], + [KEY_PHOTOS, KEY_TAGS], { + tags: [KEY_PHOTOS, KEY_TAGS], } )(); export const getUniqueCamerasCached: typeof getUniqueCameras = (...args) => unstable_cache( () => getUniqueCameras(...args), - [KEY_PHOTOS, KEY_CAMERAS, KEY_NEW_QUERY], { - tags: [KEY_PHOTOS, KEY_CAMERAS, KEY_NEW_QUERY], + [KEY_PHOTOS, KEY_CAMERAS], { + tags: [KEY_PHOTOS, KEY_CAMERAS], } )(); diff --git a/src/services/postgres.ts b/src/services/postgres.ts index ad409d50..17d430aa 100644 --- a/src/services/postgres.ts +++ b/src/services/postgres.ts @@ -196,7 +196,6 @@ const sqlGetPhotosSortedByPriority = ( const sqlGetPhotosByTag = ( limit = PHOTO_DEFAULT_LIMIT, - offset = 0, tag: string, ) => sql` @@ -204,7 +203,7 @@ const sqlGetPhotosByTag = ( WHERE ${tag}=ANY(tags) AND hidden IS NOT TRUE ORDER BY taken_at ASC - LIMIT ${limit} OFFSET ${offset} + LIMIT ${limit} `; const sqlGetPhotosByCamera = async ( @@ -220,6 +219,17 @@ const sqlGetPhotosByCamera = async ( LIMIT ${limit} `; +const sqlGetPhotosBySimulation = async ( + limit = PHOTO_DEFAULT_LIMIT, + simulation: FujifilmSimulation, +) => sql` + SELECT * FROM photos + WHERE film_simulation=${simulation} + AND hidden IS NOT TRUE + ORDER BY taken_at ASC + LIMIT ${limit} +`; + const sqlGetPhotosTakenAfterDateInclusive = ( takenAt: Date, limit?: number, @@ -270,6 +280,14 @@ const sqlGetPhotosCameraCount = async (camera: Camera) => sql` hidden IS NOT TRUE `.then(({ rows }) => parseInt(rows[0].count, 10)); +const sqlGetPhotosFilmSimulationCount = async ( + simulation: FujifilmSimulation, +) => sql` + SELECT COUNT(*) FROM photos + WHERE film_simulation=${simulation} AND + hidden IS NOT TRUE +`.then(({ rows }) => parseInt(rows[0].count, 10)); + const sqlGetPhotosTagDateRange = async (tag: string) => sql` SELECT MIN(taken_at_naive) as start, MAX(taken_at_naive) as end FROM photos @@ -286,6 +304,15 @@ const sqlGetPhotosCameraDateRange = async (camera: Camera) => sql` hidden IS NOT TRUE `.then(({ rows }) => rows[0] as PhotoDateRange); +const sqlGetPhotosFilmSimulationDateRange = async ( + simulation: FujifilmSimulation, +) => sql` + SELECT MIN(taken_at_naive) as start, MAX(taken_at_naive) as end + FROM photos + WHERE film_simulation=${simulation} AND + hidden IS NOT TRUE +`.then(({ rows }) => rows[0] as PhotoDateRange); + const sqlGetUniqueTags = async () => sql` SELECT DISTINCT unnest(tags) as tag, COUNT(*) FROM photos @@ -334,9 +361,9 @@ const sqlGetUniqueFilmSimulations = async () => sql` export type GetPhotosOptions = { sortBy?: 'createdAt' | 'takenAt' | 'priority' limit?: number - offset?: number tag?: string camera?: Camera + simulation?: FujifilmSimulation takenBefore?: Date takenAfterInclusive?: Date includeHidden?: boolean @@ -375,31 +402,33 @@ export const getPhotos = async (options: GetPhotosOptions = {}) => { const { sortBy = 'takenAt', limit, - offset, tag, camera, + simulation, takenBefore, takenAfterInclusive, includeHidden, } = options; - let getPhotosSql = () => sqlGetPhotos(limit, offset); + let getPhotosSql = () => sqlGetPhotos(limit); if (includeHidden) { - getPhotosSql = () => sqlGetPhotosIncludingHidden(limit, offset); + getPhotosSql = () => sqlGetPhotosIncludingHidden(limit); } else if (takenBefore) { getPhotosSql = () => sqlGetPhotosTakenBeforeDate(takenBefore, limit); } else if (takenAfterInclusive) { // eslint-disable-next-line max-len getPhotosSql = () => sqlGetPhotosTakenAfterDateInclusive(takenAfterInclusive, limit); } else if (tag) { - getPhotosSql = () => sqlGetPhotosByTag(limit, offset, tag); + getPhotosSql = () => sqlGetPhotosByTag(limit, tag); } else if (camera) { getPhotosSql = () => sqlGetPhotosByCamera(limit, camera.make, camera.model); + } else if (simulation) { + getPhotosSql = () => sqlGetPhotosBySimulation(limit, simulation); } else if (sortBy === 'createdAt') { - getPhotosSql = () => sqlGetPhotosSortedByCreatedAt(limit, offset); + getPhotosSql = () => sqlGetPhotosSortedByCreatedAt(limit); } else if (sortBy === 'priority') { - getPhotosSql = () => sqlGetPhotosSortedByPriority(limit, offset); + getPhotosSql = () => sqlGetPhotosSortedByPriority(limit); } return safelyQueryPhotos(getPhotosSql) @@ -439,3 +468,8 @@ export const getPhotosCameraCount = (camera: Camera) => // FILM SIMULATIONS export const getUniqueFilmSimulations = () => safelyQueryPhotos(sqlGetUniqueFilmSimulations); +export const getPhotosFilmSimulationDateRange = + (simulation: FujifilmSimulation) => safelyQueryPhotos(() => + sqlGetPhotosFilmSimulationDateRange(simulation)); +export const getPhotosFilmSimulationCount = (simulation: FujifilmSimulation) => + safelyQueryPhotos(() => sqlGetPhotosFilmSimulationCount(simulation)); From f728e3981b189c3746d17ebc722b23c0576a0f2a Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 6 Nov 2023 10:05:20 -0600 Subject: [PATCH 3/6] 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