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;