diff --git a/__tests__/path.test.ts b/__tests__/path.test.ts index 0552a40f..5bbe5016 100644 --- a/__tests__/path.test.ts +++ b/__tests__/path.test.ts @@ -10,6 +10,10 @@ import { isPathFilmSimulationPhoto, isPathFilmSimulationPhotoShare, isPathFilmSimulationShare, + isPathFocalLength, + isPathFocalLengthPhoto, + isPathFocalLengthPhotoShare, + isPathFocalLengthShare, isPathPhoto, isPathPhotoShare, isPathProtected, @@ -20,13 +24,15 @@ import { } from '@/site/paths'; import { TAG_HIDDEN } from '@/tag'; -const PHOTO_ID = 'UsKSGcbt'; -const TAG = 'tag-name'; -const CAMERA_MAKE = 'fujifilm'; -const CAMERA_MODEL = 'x-t1'; -const CAMERA_OBJECT = { make: CAMERA_MAKE, model: CAMERA_MODEL }; -const FILM_SIMULATION = 'acros'; -const SHARE = 'share'; +const PHOTO_ID = 'UsKSGcbt'; +const TAG = 'tag-name'; +const CAMERA_MAKE = 'fujifilm'; +const CAMERA_MODEL = 'x-t1'; +const CAMERA_OBJECT = { make: CAMERA_MAKE, model: CAMERA_MODEL }; +const FILM_SIMULATION = 'acros'; +const FOCAL_LENGTH = 90; +const FOCAL_LENGTH_STRING = `${FOCAL_LENGTH}mm`; +const SHARE = 'share'; const PATH_ROOT = '/'; const PATH_GRID = '/grid'; @@ -54,6 +60,11 @@ 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}`; + +const PATH_FOCAL_LENGTH = `/focal/${FOCAL_LENGTH_STRING}`; +const PATH_FOCAL_LENGTH_SHARE = `${PATH_FOCAL_LENGTH}/${SHARE}`; +const PATH_FOCAL_LENGTH_PHOTO = `${PATH_FOCAL_LENGTH}/${PHOTO_ID}`; +const PATH_FOCAL_LENGTH_PHOTO_SHARE = `${PATH_FOCAL_LENGTH_PHOTO}/${SHARE}`; describe('Paths', () => { it('can be protected', () => { @@ -87,6 +98,10 @@ describe('Paths', () => { 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); + expect(isPathFocalLength(PATH_FOCAL_LENGTH)).toBe(true); + expect(isPathFocalLengthShare(PATH_FOCAL_LENGTH_SHARE)).toBe(true); + expect(isPathFocalLengthPhoto(PATH_FOCAL_LENGTH_PHOTO)).toBe(true); + expect(isPathFocalLengthPhotoShare(PATH_FOCAL_LENGTH_PHOTO_SHARE)).toBe(true); // Negative expect(isPathPhoto(PATH_TAG_PHOTO_SHARE)).toBe(false); expect(isPathPhotoShare(PATH_TAG_PHOTO)).toBe(false); @@ -102,8 +117,13 @@ describe('Paths', () => { expect(isPathFilmSimulationShare(PATH_TAG)).toBe(false); expect(isPathFilmSimulationPhoto(PATH_PHOTO_SHARE)).toBe(false); expect(isPathFilmSimulationPhotoShare(PATH_PHOTO)).toBe(false); + expect(isPathFocalLength(PATH_FILM_SIMULATION)).toBe(false); + expect(isPathFocalLengthShare(PATH_FILM_SIMULATION_SHARE)).toBe(false); + expect(isPathFocalLengthPhoto(PATH_FILM_SIMULATION_PHOTO)).toBe(false); + expect(isPathFocalLengthPhotoShare(PATH_FILM_SIMULATION_PHOTO_SHARE)).toBe(false); }); it('can be parsed', () => { + // Core expect(getPathComponents(PATH_ROOT)).toEqual({}); expect(getPathComponents(PATH_PHOTO)).toEqual({ photoId: PHOTO_ID, @@ -111,6 +131,7 @@ describe('Paths', () => { expect(getPathComponents(PATH_PHOTO_SHARE)).toEqual({ photoId: PHOTO_ID, }); + // Tag expect(getPathComponents(PATH_TAG)).toEqual({ tag: TAG, }); @@ -125,6 +146,7 @@ describe('Paths', () => { photoId: PHOTO_ID, tag: TAG, }); + // Camera expect(getPathComponents(PATH_CAMERA)).toEqual({ camera: CAMERA_OBJECT, }); @@ -139,6 +161,7 @@ describe('Paths', () => { photoId: PHOTO_ID, camera: CAMERA_OBJECT, }); + // Film Simulation expect(getPathComponents(PATH_FILM_SIMULATION)).toEqual({ simulation: FILM_SIMULATION, }); @@ -153,29 +176,49 @@ describe('Paths', () => { photoId: PHOTO_ID, simulation: FILM_SIMULATION, }); + // Focal Length + expect(getPathComponents(PATH_FOCAL_LENGTH)).toEqual({ + focal: FOCAL_LENGTH, + }); + expect(getPathComponents(PATH_FOCAL_LENGTH_SHARE)).toEqual({ + focal: FOCAL_LENGTH, + }); + expect(getPathComponents(PATH_FOCAL_LENGTH_PHOTO)).toEqual({ + photoId: PHOTO_ID, + focal: FOCAL_LENGTH, + }); + expect(getPathComponents(PATH_FOCAL_LENGTH_PHOTO_SHARE)).toEqual({ + photoId: PHOTO_ID, + focal: FOCAL_LENGTH, + }); }); it('can be escaped', () => { - // Root views + // Root expect(getEscapePath(PATH_ROOT)).toEqual(undefined); expect(getEscapePath(PATH_GRID)).toEqual(undefined); expect(getEscapePath(PATH_ADMIN)).toEqual(undefined); - // Photo views + // Photo expect(getEscapePath(PATH_PHOTO)).toEqual(PATH_GRID); expect(getEscapePath(PATH_PHOTO_SHARE)).toEqual(PATH_PHOTO); - // Tag views + // Tag expect(getEscapePath(PATH_TAG)).toEqual(PATH_GRID); expect(getEscapePath(PATH_TAG_SHARE)).toEqual(PATH_TAG); expect(getEscapePath(PATH_TAG_PHOTO)).toEqual(PATH_TAG); expect(getEscapePath(PATH_TAG_PHOTO_SHARE)).toEqual(PATH_TAG_PHOTO); - // Camera views + // Camera expect(getEscapePath(PATH_CAMERA)).toEqual(PATH_GRID); expect(getEscapePath(PATH_CAMERA_SHARE)).toEqual(PATH_CAMERA); expect(getEscapePath(PATH_CAMERA_PHOTO)).toEqual(PATH_CAMERA); expect(getEscapePath(PATH_CAMERA_PHOTO_SHARE)).toEqual(PATH_CAMERA_PHOTO); - // Film Simulation views + // Film Simulation 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); + // Focal Length + expect(getEscapePath(PATH_FOCAL_LENGTH)).toEqual(PATH_GRID); + expect(getEscapePath(PATH_FOCAL_LENGTH_SHARE)).toEqual(PATH_FOCAL_LENGTH); + expect(getEscapePath(PATH_FOCAL_LENGTH_PHOTO)).toEqual(PATH_FOCAL_LENGTH); + expect(getEscapePath(PATH_FOCAL_LENGTH_PHOTO_SHARE)).toEqual(PATH_FOCAL_LENGTH_PHOTO); }); }); diff --git a/src/admin/AdminPhotoMenuClient.tsx b/src/admin/AdminPhotoMenuClient.tsx index 60b902a7..ee792f59 100644 --- a/src/admin/AdminPhotoMenuClient.tsx +++ b/src/admin/AdminPhotoMenuClient.tsx @@ -25,7 +25,7 @@ export default function AdminPhotoMenuClient({ const isFav = isPhotoFav(photo); const path = usePathname(); const shouldRedirectFav = isPathFavs(path) && isFav; - const shouldRedirectDelete = pathForPhoto(photo.id) === path; + const shouldRedirectDelete = pathForPhoto({ photo: photo.id }) === path; return ( isUserSignedIn diff --git a/src/admin/AdminPhotosTable.tsx b/src/admin/AdminPhotosTable.tsx index 34dcf7de..15d6c6fc 100644 --- a/src/admin/AdminPhotosTable.tsx +++ b/src/admin/AdminPhotosTable.tsx @@ -45,7 +45,7 @@ export default function AdminPhotosTable({
diff --git a/src/app/film/[simulation]/[photoId]/layout.tsx b/src/app/film/[simulation]/[photoId]/layout.tsx index 4773e8da..4219e838 100644 --- a/src/app/film/[simulation]/[photoId]/layout.tsx +++ b/src/app/film/[simulation]/[photoId]/layout.tsx @@ -41,7 +41,7 @@ export async function generateMetadata({ const title = titleForPhoto(photo); const description = descriptionForPhoto(photo); const images = absolutePathForPhotoImage(photo); - const url = absolutePathForPhoto(photo, simulation); + const url = absolutePathForPhoto({ photo, simulation }); return { title, diff --git a/src/app/focal/[focal]/[photoId]/layout.tsx b/src/app/focal/[focal]/[photoId]/layout.tsx new file mode 100644 index 00000000..868d8c4d --- /dev/null +++ b/src/app/focal/[focal]/[photoId]/layout.tsx @@ -0,0 +1,86 @@ +import { + RELATED_GRID_PHOTOS_TO_SHOW, + descriptionForPhoto, + titleForPhoto, +} from '@/photo'; +import { Metadata } from 'next/types'; +import { redirect } from 'next/navigation'; +import { + PATH_ROOT, + absolutePathForPhoto, + absolutePathForPhotoImage, +} from '@/site/paths'; +import PhotoDetailPage from '@/photo/PhotoDetailPage'; +import { getPhotosNearIdCached } from '@/photo/cache'; +import { ReactNode, cache } from 'react'; +import { getPhotosMeta } from '@/photo/db/query'; +import { getFocalLengthFromString } from '@/focal'; + +const getPhotosNearIdCachedCached = cache((photoId: string, focal: number) => + getPhotosNearIdCached( + photoId, + { focal, limit: RELATED_GRID_PHOTOS_TO_SHOW + 2 }, + )); + +interface PhotoFocalLengthProps { + params: { photoId: string, focal: string } +} + +export async function generateMetadata({ + params: { photoId, focal: focalString }, +}: PhotoFocalLengthProps): Promise { + const focal = getFocalLengthFromString(focalString); + + const { photo } = await getPhotosNearIdCachedCached(photoId, focal); + + if (!photo) { return {}; } + + const title = titleForPhoto(photo); + const description = descriptionForPhoto(photo); + const images = absolutePathForPhotoImage(photo); + const url = absolutePathForPhoto({ photo, focal }); + + return { + title, + description, + openGraph: { + title, + images, + description, + url, + }, + twitter: { + title, + description, + images, + card: 'summary_large_image', + }, + }; +} + +export default async function PhotoFocalLengthPage({ + params: { photoId, focal: focalString }, + children, +}: PhotoFocalLengthProps & { children: ReactNode }) { + const focal = getFocalLengthFromString(focalString); + + const { photo, photos, photosGrid, indexNumber } = + await getPhotosNearIdCachedCached(photoId, focal); + + if (!photo) { redirect(PATH_ROOT); } + + const { count, dateRange } = await getPhotosMeta({ focal }); + + return <> + {children} + + ; +} diff --git a/src/app/focal/[focal]/[photoId]/page.tsx b/src/app/focal/[focal]/[photoId]/page.tsx new file mode 100644 index 00000000..67e08591 --- /dev/null +++ b/src/app/focal/[focal]/[photoId]/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return null; +} diff --git a/src/app/focal/[focal]/[photoId]/share/page.tsx b/src/app/focal/[focal]/[photoId]/share/page.tsx new file mode 100644 index 00000000..90702366 --- /dev/null +++ b/src/app/focal/[focal]/[photoId]/share/page.tsx @@ -0,0 +1,19 @@ +import { getFocalLengthFromString } from '@/focal'; +import { getPhotoCached } from '@/photo/cache'; +import PhotoShareModal from '@/photo/PhotoShareModal'; +import { PATH_ROOT } from '@/site/paths'; +import { redirect } from 'next/navigation'; + +export default async function Share({ + params: { photoId, focal: focalString }, +}: { + params: { photoId: string, focal: string } +}) { + const focal = getFocalLengthFromString(focalString); + + const photo = await getPhotoCached(photoId); + + if (!photo) { return redirect(PATH_ROOT); } + + return ; +} diff --git a/src/app/focal/[focal]/image/route.tsx b/src/app/focal/[focal]/image/route.tsx new file mode 100644 index 00000000..22db2d06 --- /dev/null +++ b/src/app/focal/[focal]/image/route.tsx @@ -0,0 +1,41 @@ +import { getPhotosCached } from '@/photo/cache'; +import { + IMAGE_OG_DIMENSION_SMALL, + MAX_PHOTOS_TO_SHOW_PER_TAG, +} from '@/image-response'; +import { getIBMPlexMonoMedium } from '@/site/font'; +import { ImageResponse } from 'next/og'; +import { getImageResponseCacheControlHeaders } from '@/image-response/cache'; +import FocalLengthImageResponse from + '@/image-response/FocalLengthImageResponse'; +import { getFocalLengthFromString } from '@/focal'; + +export async function GET( + _: Request, + context: { params: { focal: string } }, +) { + const focal = getFocalLengthFromString(context.params.focal); + + const [ + photos, + { fontFamily, fonts }, + headers, + ] = await Promise.all([ + getPhotosCached({ limit: MAX_PHOTOS_TO_SHOW_PER_TAG, focal }), + getIBMPlexMonoMedium(), + getImageResponseCacheControlHeaders(), + ]); + + const { width, height } = IMAGE_OG_DIMENSION_SMALL; + + return new ImageResponse( + , + { width, height, fonts, headers }, + ); +} diff --git a/src/app/focal/[focal]/page.tsx b/src/app/focal/[focal]/page.tsx new file mode 100644 index 00000000..82948194 --- /dev/null +++ b/src/app/focal/[focal]/page.tsx @@ -0,0 +1,71 @@ +import { generateMetaForFocalLength, getFocalLengthFromString } from '@/focal'; +import FocalLengthOverview from '@/focal/FocalLengthOverview'; +import { getPhotosFocalLengthDataCached } from '@/focal/data'; +import { INFINITE_SCROLL_GRID_PHOTO_INITIAL } from '@/photo'; +import { PATH_ROOT } from '@/site/paths'; +import type { Metadata } from 'next'; +import { redirect } from 'next/navigation'; +import { cache } from 'react'; + +const getPhotosFocalDataCachedCached = cache((focal: number) => + getPhotosFocalLengthDataCached({ + focal, + limit: INFINITE_SCROLL_GRID_PHOTO_INITIAL, + })); + +interface FocalLengthProps { + params: { focal: string } +} + +export async function generateMetadata({ + params: { focal: focalString }, +}: FocalLengthProps): Promise { + const focal = getFocalLengthFromString(focalString); + + const [ + photos, + { count, dateRange }, + ] = await getPhotosFocalDataCachedCached(focal); + + if (photos.length === 0) { return {}; } + + const { + url, + title, + description, + images, + } = generateMetaForFocalLength(focal, photos, count, dateRange); + + return { + title, + openGraph: { + title, + description, + images, + url, + }, + twitter: { + images, + description, + card: 'summary_large_image', + }, + description, + }; +} + +export default async function TagPage({ + params: { focal: focalString }, +}:FocalLengthProps) { + const focal = getFocalLengthFromString(focalString); + + const [ + photos, + { count, dateRange }, + ] = await getPhotosFocalDataCachedCached(focal); + + if (photos.length === 0) { redirect(PATH_ROOT); } + + return ( + + ); +} diff --git a/src/app/focal/[focal]/share/page.tsx b/src/app/focal/[focal]/share/page.tsx new file mode 100644 index 00000000..01d7aead --- /dev/null +++ b/src/app/focal/[focal]/share/page.tsx @@ -0,0 +1,70 @@ +import { generateMetaForFocalLength, getFocalLengthFromString } from '@/focal'; +import FocalLengthOverview from '@/focal/FocalLengthOverview'; +import FocalLengthShareModal from '@/focal/FocalLengthShareModal'; +import { getPhotosFocalLengthDataCached } from '@/focal/data'; +import { INFINITE_SCROLL_GRID_PHOTO_INITIAL } from '@/photo'; +import type { Metadata } from 'next'; +import { cache } from 'react'; + +const getPhotosFocalLengthDataCachedCached = cache((focal: number) => + getPhotosFocalLengthDataCached({ + focal, + limit: INFINITE_SCROLL_GRID_PHOTO_INITIAL, + })); + +interface FocalLengthProps { + params: { focal: string } +} + +export async function generateMetadata({ + params: { focal: focalString }, +}: FocalLengthProps): Promise { + const focal = getFocalLengthFromString(focalString); + + const [ + photos, + { count, dateRange }, + ] = await getPhotosFocalLengthDataCachedCached(focal); + + const { + url, + title, + description, + images, + } = generateMetaForFocalLength(focal, photos, count, dateRange); + + return { + title, + openGraph: { + title, + description, + images, + url, + }, + twitter: { + images, + description, + card: 'summary_large_image', + }, + description, + }; +} + +export default async function Share({ + params: { focal: focalString }, +}: FocalLengthProps) { + const focal = getFocalLengthFromString(focalString); + + const [ + photos, + { count, dateRange }, + ] = await getPhotosFocalLengthDataCachedCached(focal); + + return <> + + + ; +} diff --git a/src/app/p/[photoId]/layout.tsx b/src/app/p/[photoId]/layout.tsx index e40e7d9c..1a00868a 100644 --- a/src/app/p/[photoId]/layout.tsx +++ b/src/app/p/[photoId]/layout.tsx @@ -44,7 +44,7 @@ export async function generateMetadata({ const title = titleForPhoto(photo); const description = descriptionForPhoto(photo); const images = absolutePathForPhotoImage(photo); - const url = absolutePathForPhoto(photo); + const url = absolutePathForPhoto({ photo }); return { title, diff --git a/src/app/shot-on/[make]/[model]/[photoId]/layout.tsx b/src/app/shot-on/[make]/[model]/[photoId]/layout.tsx index 42efcca0..b2908928 100644 --- a/src/app/shot-on/[make]/[model]/[photoId]/layout.tsx +++ b/src/app/shot-on/[make]/[model]/[photoId]/layout.tsx @@ -44,11 +44,10 @@ export async function generateMetadata({ const title = titleForPhoto(photo); const description = descriptionForPhoto(photo); const images = absolutePathForPhotoImage(photo); - const url = absolutePathForPhoto( + const url = absolutePathForPhoto({ photo, - undefined, - cameraFromPhoto(photo, { make, model }), - ); + camera: cameraFromPhoto(photo, { make, model }), + }); return { title, diff --git a/src/app/tag/[tag]/[photoId]/layout.tsx b/src/app/tag/[tag]/[photoId]/layout.tsx index 5de147fd..cb8ebd85 100644 --- a/src/app/tag/[tag]/[photoId]/layout.tsx +++ b/src/app/tag/[tag]/[photoId]/layout.tsx @@ -35,7 +35,7 @@ export async function generateMetadata({ const title = titleForPhoto(photo); const description = descriptionForPhoto(photo); const images = absolutePathForPhotoImage(photo); - const url = absolutePathForPhoto(photo, tag); + const url = absolutePathForPhoto({ photo, tag }); return { title, diff --git a/src/app/tag/hidden/[photoId]/page.tsx b/src/app/tag/hidden/[photoId]/page.tsx index 954fca0c..796447c0 100644 --- a/src/app/tag/hidden/[photoId]/page.tsx +++ b/src/app/tag/hidden/[photoId]/page.tsx @@ -33,7 +33,7 @@ export async function generateMetadata({ const title = titleForPhoto(photo); const description = descriptionForPhoto(photo); - const url = absolutePathForPhoto(photo, TAG_HIDDEN); + const url = absolutePathForPhoto({ photo, tag: TAG_HIDDEN }); return { title, diff --git a/src/components/CommandKClient.tsx b/src/components/CommandKClient.tsx index 4f5af280..61d6bd10 100644 --- a/src/components/CommandKClient.tsx +++ b/src/components/CommandKClient.tsx @@ -147,7 +147,7 @@ export default function CommandKClient({ keywords: getKeywordsForPhoto(photo), annotation: , accessory: , - path: pathForPhoto(photo), + path: pathForPhoto({ photo }), })), }] : []); diff --git a/src/focal/FocalLengthHeader.tsx b/src/focal/FocalLengthHeader.tsx new file mode 100644 index 00000000..b40f459a --- /dev/null +++ b/src/focal/FocalLengthHeader.tsx @@ -0,0 +1,38 @@ +import { Photo, PhotoDateRange } from '@/photo'; +import { descriptionForFocalLengthPhotos } from '.'; +import { pathForFocalLengthShare } from '@/site/paths'; +import PhotoSetHeader from '@/photo/PhotoSetHeader'; +import PhotoFocalLength from './PhotoFocalLength'; + +export default function FocalLengthHeader({ + focal, + photos, + selectedPhoto, + indexNumber, + count, + dateRange, +}: { + focal: number + photos: Photo[] + selectedPhoto?: Photo + indexNumber?: number + count?: number + dateRange?: PhotoDateRange +}) { + return ( + } + entityDescription={descriptionForFocalLengthPhotos( + photos, + undefined, + count, + )} + photos={photos} + selectedPhoto={selectedPhoto} + sharePath={pathForFocalLengthShare(focal)} + indexNumber={indexNumber} + count={count} + dateRange={dateRange} + /> + ); +} diff --git a/src/focal/FocalLengthOGTile.tsx b/src/focal/FocalLengthOGTile.tsx new file mode 100644 index 00000000..29e9af83 --- /dev/null +++ b/src/focal/FocalLengthOGTile.tsx @@ -0,0 +1,50 @@ +import { Photo, PhotoDateRange } from '@/photo'; +import { + absolutePathForFocalLengthImage, + pathForFocalLength, +} from '@/site/paths'; +import OGTile from '@/components/OGTile'; +import { descriptionForFocalLengthPhotos, titleForFocalLength } from '.'; + +export type OGLoadingState = 'unloaded' | 'loading' | 'loaded' | 'failed'; + +export default function FocalLengthOGTile({ + focal, + photos, + loadingState: loadingStateExternal, + riseOnHover, + onLoad, + onFail, + retryTime, + count, + dateRange, +}: { + focal: number + photos: Photo[] + loadingState?: OGLoadingState + onLoad?: () => void + onFail?: () => void + riseOnHover?: boolean + retryTime?: number + count?: number + dateRange?: PhotoDateRange +}) { + return ( + + ); +}; diff --git a/src/focal/FocalLengthOverview.tsx b/src/focal/FocalLengthOverview.tsx new file mode 100644 index 00000000..a5d23463 --- /dev/null +++ b/src/focal/FocalLengthOverview.tsx @@ -0,0 +1,33 @@ +import { Photo, PhotoDateRange } from '@/photo'; +import PhotoGridPage from '@/photo/PhotoGridPage'; +import FocalLengthHeader from './FocalLengthHeader'; + +export default function FocalLengthOverview({ + focal, + photos, + count, + dateRange, + animateOnFirstLoadOnly, +}: { + focal: number, + photos: Photo[], + count: number, + dateRange?: PhotoDateRange, + animateOnFirstLoadOnly?: boolean, +}) { + return ( + , + animateOnFirstLoadOnly, + }} /> + ); +} diff --git a/src/focal/FocalLengthShareModal.tsx b/src/focal/FocalLengthShareModal.tsx new file mode 100644 index 00000000..62b02b41 --- /dev/null +++ b/src/focal/FocalLengthShareModal.tsx @@ -0,0 +1,26 @@ +import { absolutePathForFocalLength, pathForFocalLength } from '@/site/paths'; +import { Photo, PhotoDateRange } from '../photo'; +import ShareModal from '@/components/ShareModal'; +import FocalLengthOGTile from './FocalLengthOGTile'; + +export default function FocalLengthShareModal({ + focal, + photos, + count, + dateRange, +}: { + focal: number + photos: Photo[] + count?: number + dateRange?: PhotoDateRange +}) { + return ( + + + + ); +}; diff --git a/src/focal/PhotoFocalLength.tsx b/src/focal/PhotoFocalLength.tsx new file mode 100644 index 00000000..b242e43b --- /dev/null +++ b/src/focal/PhotoFocalLength.tsx @@ -0,0 +1,31 @@ +import { pathForFocalLength } from '@/site/paths'; +import EntityLink, { + EntityLinkExternalProps, +} from '@/components/primitives/EntityLink'; +import { TbCone } from 'react-icons/tb'; +import { formatFocalLength } from '.'; + +export default function PhotoFocalLength({ + focal, + type, + badged, + contrast, + prefetch, + countOnHover, +}: { + focal: number + countOnHover?: number +} & EntityLinkExternalProps) { + return ( + } + type={type} + badged={badged} + contrast={contrast} + prefetch={prefetch} + hoverEntity={countOnHover} + /> + ); +} diff --git a/src/focal/data.ts b/src/focal/data.ts new file mode 100644 index 00000000..3001d7fb --- /dev/null +++ b/src/focal/data.ts @@ -0,0 +1,17 @@ +import { + getPhotosCached, + getPhotosMetaCached, +} from '@/photo/cache'; + +export const getPhotosFocalLengthDataCached = ({ + focal, + limit, +}: { + focal: number, + limit?: number, +}) => + Promise.all([ + getPhotosCached({ focal, limit }), + getPhotosMetaCached({ focal }), + ]); + diff --git a/src/focal/index.ts b/src/focal/index.ts new file mode 100644 index 00000000..f3f40bd3 --- /dev/null +++ b/src/focal/index.ts @@ -0,0 +1,59 @@ +import { + Photo, + PhotoDateRange, + descriptionForPhotoSet, + photoQuantityText, +} from '@/photo'; +import { + absolutePathForFocalLength, + absolutePathForFocalLengthImage, +} from '@/site/paths'; + +export const getFocalLengthFromString = (focalString?: string) => { + const focal = focalString?.match(/^([0-9]+)mm/)?.[1]; + return focal ? parseInt(focal, 10) : 0; +}; + +export const formatFocalLength = (focal?: number) => focal ? + `${focal}mm` + : undefined; + +export const titleForFocalLength = ( + focal: number, + photos: Photo[], + explicitCount?: number, +) => [ + `${formatFocalLength(focal)} Focal Length`, + photoQuantityText(explicitCount ?? photos.length), +].join(' '); + +export const descriptionForFocalLengthPhotos = ( + photos: Photo[], + dateBased?: boolean, + explicitCount?: number, + explicitDateRange?: PhotoDateRange, +) => + descriptionForPhotoSet( + photos, + undefined, + dateBased, + explicitCount, + explicitDateRange, + ); + +export const generateMetaForFocalLength = ( + focal: number, + photos: Photo[], + explicitCount?: number, + explicitDateRange?: PhotoDateRange, +) => ({ + url: absolutePathForFocalLength(focal), + title: titleForFocalLength(focal, photos, explicitCount), + description: descriptionForFocalLengthPhotos( + photos, + true, + explicitCount, + explicitDateRange, + ), + images: absolutePathForFocalLengthImage(focal), +}); diff --git a/src/image-response/FocalLengthImageResponse.tsx b/src/image-response/FocalLengthImageResponse.tsx new file mode 100644 index 00000000..498f9cfd --- /dev/null +++ b/src/image-response/FocalLengthImageResponse.tsx @@ -0,0 +1,46 @@ +import type { Photo } from '../photo'; +import ImageCaption from './components/ImageCaption'; +import ImagePhotoGrid from './components/ImagePhotoGrid'; +import ImageContainer from './components/ImageContainer'; +import type { NextImageSize } from '@/services/next-image'; +import { TbCone } from 'react-icons/tb'; +import { formatFocalLength } from '@/focal'; + +export default function FocalLengthImageResponse({ + focal, + photos, + width, + height, + fontFamily, +}: { + focal: number, + photos: Photo[] + width: NextImageSize + height: number + fontFamily: string +}) { + return ( + + + + + {formatFocalLength(focal)} + + + ); +} diff --git a/src/photo/PhotoDetailPage.tsx b/src/photo/PhotoDetailPage.tsx index 6f95b22a..c96b8bf2 100644 --- a/src/photo/PhotoDetailPage.tsx +++ b/src/photo/PhotoDetailPage.tsx @@ -12,6 +12,7 @@ import { FilmSimulation } from '@/simulation'; import FilmSimulationHeader from '@/simulation/FilmSimulationHeader'; import { TAG_HIDDEN } from '@/tag'; import HiddenHeader from '@/tag/HiddenHeader'; +import FocalLengthHeader from '@/focal/FocalLengthHeader'; export default function PhotoDetailPage({ photo, @@ -20,6 +21,7 @@ export default function PhotoDetailPage({ tag, camera, simulation, + focal, indexNumber, count, dateRange, @@ -30,6 +32,7 @@ export default function PhotoDetailPage({ tag?: string camera?: Camera simulation?: FilmSimulation + focal?: number indexNumber?: number count?: number dateRange?: PhotoDateRange @@ -82,6 +85,19 @@ export default function PhotoDetailPage({ dateRange={dateRange} />} />} + {focal && + } + />} } contentSide={
, ]} diff --git a/src/photo/PhotoGrid.tsx b/src/photo/PhotoGrid.tsx index 32e669d4..6dbc57f4 100644 --- a/src/photo/PhotoGrid.tsx +++ b/src/photo/PhotoGrid.tsx @@ -12,6 +12,7 @@ export default function PhotoGrid({ tag, camera, simulation, + focal, photoPriority, fast, animate = true, @@ -28,6 +29,7 @@ export default function PhotoGrid({ tag?: string camera?: Camera simulation?: FilmSimulation + focal?: number photoPriority?: boolean fast?: boolean animate?: boolean @@ -77,6 +79,7 @@ export default function PhotoGrid({ tag, camera, simulation, + focal, selected: photo.id === selectedPhoto?.id, priority: photoPriority, onVisible: index === photos.length - 1 diff --git a/src/photo/PhotoGridInfinite.tsx b/src/photo/PhotoGridInfinite.tsx index 98b43667..0b9a1428 100644 --- a/src/photo/PhotoGridInfinite.tsx +++ b/src/photo/PhotoGridInfinite.tsx @@ -13,6 +13,7 @@ export default function PhotoGridInfinite({ tag, camera, simulation, + focal, animateOnFirstLoadOnly, }: { cacheKey: string @@ -21,6 +22,7 @@ export default function PhotoGridInfinite({ tag?: string camera?: Camera simulation?: FilmSimulation + focal?: number animateOnFirstLoadOnly?: boolean }) { return ( @@ -39,6 +41,7 @@ export default function PhotoGridInfinite({ tag, camera, simulation, + focal, onLastPhotoVisible, animateOnFirstLoadOnly, }} />} diff --git a/src/photo/PhotoGridPage.tsx b/src/photo/PhotoGridPage.tsx index 4e1fdc74..eb3430f0 100644 --- a/src/photo/PhotoGridPage.tsx +++ b/src/photo/PhotoGridPage.tsx @@ -17,6 +17,7 @@ export default function PhotoGridPage({ tag, camera, simulation, + focal, animateOnFirstLoadOnly, header, sidebar, @@ -27,6 +28,7 @@ export default function PhotoGridPage({ tag?: string camera?: Camera simulation?: FilmSimulation + focal?: number animateOnFirstLoadOnly?: boolean header?: JSX.Element sidebar?: JSX.Element @@ -58,6 +60,7 @@ export default function PhotoGridPage({ tag, camera, simulation, + focal, animateOnFirstLoadOnly, onAnimationComplete, }} /> @@ -69,6 +72,7 @@ export default function PhotoGridPage({ tag, camera, simulation, + focal, animateOnFirstLoadOnly, }} />} diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index 92862170..b0331219 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -11,7 +11,11 @@ import SiteGrid from '@/components/SiteGrid'; import ImageLarge from '@/components/image/ImageLarge'; import { clsx } from 'clsx/lite'; import Link from 'next/link'; -import { pathForPhoto, pathForPhotoShare } from '@/site/paths'; +import { + pathForFocalLength, + pathForPhoto, + pathForPhotoShare, +} from '@/site/paths'; import PhotoTags from '@/tag/PhotoTags'; import ShareButton from '@/components/ShareButton'; import PhotoCamera from '../camera/PhotoCamera'; @@ -40,6 +44,7 @@ export default function PhotoLarge({ shouldShareTag, shouldShareCamera, shouldShareSimulation, + shouldShareFocalLength, shouldScrollOnShare, onVisible, }: { @@ -54,6 +59,7 @@ export default function PhotoLarge({ shouldShareTag?: boolean shouldShareCamera?: boolean shouldShareSimulation?: boolean + shouldShareFocalLength?: boolean shouldScrollOnShare?: boolean onVisible?: () => void }) { @@ -76,7 +82,7 @@ export default function PhotoLarge({ containerRef={ref} contentMain={