diff --git a/app/lens/[make]/[model]/[photoId]/page.tsx b/app/lens/[make]/[model]/[photoId]/page.tsx new file mode 100644 index 00000000..8d1ca67e --- /dev/null +++ b/app/lens/[make]/[model]/[photoId]/page.tsx @@ -0,0 +1,93 @@ +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 '@/app/paths'; +import PhotoDetailPage from '@/photo/PhotoDetailPage'; +import { + getPhotosMetaCached, + getPhotosNearIdCached, +} from '@/photo/cache'; +import { cache } from 'react'; +import { getLensFromParams, lensFromPhoto, PhotoLensProps } from '@/lens'; + +const getPhotosNearIdCachedCached = cache(( + photoId: string, + make: string, + model: string, +) => + getPhotosNearIdCached( + photoId, { + lens: getLensFromParams({ make, model }), + limit: RELATED_GRID_PHOTOS_TO_SHOW + 2, + }, + )); + +export async function generateMetadata({ + params, +}: PhotoLensProps): Promise { + const { photoId, make, model } = await params; + + const { photo } = await getPhotosNearIdCachedCached(photoId, make, model); + + if (!photo) { return {}; } + + const title = titleForPhoto(photo); + const description = descriptionForPhoto(photo); + const images = absolutePathForPhotoImage(photo); + const url = absolutePathForPhoto({ + photo, + lens: lensFromPhoto(photo, { make, model }), + }); + + return { + title, + description, + openGraph: { + title, + images, + description, + url, + }, + twitter: { + title, + description, + images, + card: 'summary_large_image', + }, + }; +} + +export default async function PhotoLensPage({ + params, +}: PhotoLensProps) { + const { photoId, make, model } = await params; + + const { photo, photos, photosGrid, indexNumber } = + await getPhotosNearIdCachedCached(photoId, make, model); + + if (!photo) { redirect(PATH_ROOT); } + + const lens = lensFromPhoto(photo, { make, model }); + + const { count, dateRange } = await getPhotosMetaCached({ lens }); + + return ( + + ); +} diff --git a/app/lens/[make]/[model]/image/route.tsx b/app/lens/[make]/[model]/image/route.tsx new file mode 100644 index 00000000..7b7fd98e --- /dev/null +++ b/app/lens/[make]/[model]/image/route.tsx @@ -0,0 +1,61 @@ +import { getPhotosCached } from '@/photo/cache'; +import { + IMAGE_OG_DIMENSION_SMALL, + MAX_PHOTOS_TO_SHOW_PER_CATEGORY, +} from '@/image-response'; +import { getIBMPlexMono } from '@/app/font'; +import { ImageResponse } from 'next/og'; +import { getImageResponseCacheControlHeaders } from '@/image-response/cache'; +import { GENERATE_STATIC_PARAMS_LIMIT } from '@/photo/db'; +import { getUniqueLenses } from '@/photo/db/query'; +import { + STATICALLY_OPTIMIZED_PHOTO_CATEGORY_OG_IMAGES, + IS_PRODUCTION, +} from '@/app/config'; +import { getLensFromParams, Lens, LensProps } from '@/lens'; +import LensImageResponse from '@/image-response/LensImageResponse'; + +export let generateStaticParams: + (() => Promise<{ lens: Lens }[]>) | undefined = undefined; + +if (STATICALLY_OPTIMIZED_PHOTO_CATEGORY_OG_IMAGES && IS_PRODUCTION) { + generateStaticParams = async () => { + const lenses = await getUniqueLenses(); + return lenses + .slice(0, GENERATE_STATIC_PARAMS_LIMIT) + .map(({ lens }) => ({ lens })); + }; +} + +export async function GET( + _: Request, + context: LensProps, +) { + const lens = getLensFromParams(await context.params); + + const [ + photos, + { fontFamily, fonts }, + headers, + ] = await Promise.all([ + getPhotosCached({ + limit: MAX_PHOTOS_TO_SHOW_PER_CATEGORY, + lens: lens, + }), + getIBMPlexMono(), + getImageResponseCacheControlHeaders(), + ]); + + const { width, height } = IMAGE_OG_DIMENSION_SMALL; + + return new ImageResponse( + , + { width, height, fonts, headers }, + ); +} diff --git a/app/lens/[make]/[model]/page.tsx b/app/lens/[make]/[model]/page.tsx new file mode 100644 index 00000000..355ff2d5 --- /dev/null +++ b/app/lens/[make]/[model]/page.tsx @@ -0,0 +1,80 @@ +import { Metadata } from 'next/types'; +import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo'; +import { cache } from 'react'; +import { STATICALLY_OPTIMIZED_PHOTO_CATEGORIES } from '@/app/config'; +import { IS_PRODUCTION } from '@/app/config'; +import { getUniqueLenses } from '@/photo/db/query'; +import { generateMetaForLens } from '@/lens/meta'; +import { getPhotosLensDataCached } from '@/lens/data'; +import LensOverview from '@/lens/LensOverview'; +import { LensProps } from '@/lens'; + +const getPhotosLensDataCachedCached = cache(( + make: string, + model: string, +) => getPhotosLensDataCached( + make, + model, + INFINITE_SCROLL_GRID_INITIAL, +)); + +export let generateStaticParams: + (() => Promise<{ make: string, model: string }[]>) | undefined = undefined; + +if (STATICALLY_OPTIMIZED_PHOTO_CATEGORIES && IS_PRODUCTION) { + generateStaticParams = async () => { + const lenses = await getUniqueLenses(); + return lenses.map(({ lens: { make, model } }) => ({ make, model })); + }; +} + +export async function generateMetadata({ + params, +}: LensProps): Promise { + const { make, model } = await params; + + const [ + photos, + { count, dateRange }, + lens, + ] = await getPhotosLensDataCachedCached(make, model); + + const { + url, + title, + description, + images, + } = generateMetaForLens(lens, photos, count, dateRange); + + return { + title, + openGraph: { + title, + description, + images, + url, + }, + twitter: { + images, + description, + card: 'summary_large_image', + }, + description, + }; +} + +export default async function LensPage({ + params, +}: LensProps) { + const { make, model } = await params; + + const [ + photos, + { count, dateRange }, + lens, + ] = await getPhotosLensDataCachedCached(make, model); + + return ( + + ); +} diff --git a/src/app/paths.ts b/src/app/paths.ts index 4cc75ad8..a62a4a37 100644 --- a/src/app/paths.ts +++ b/src/app/paths.ts @@ -163,6 +163,9 @@ export const absolutePathForTag = (tag: string) => export const absolutePathForCamera= (camera: Camera) => `${BASE_URL}${pathForCamera(camera)}`; +export const absolutePathForLens= (lens: Lens) => + `${BASE_URL}${pathForLens(lens)}`; + export const absolutePathForFilmSimulation = (simulation: FilmSimulation) => `${BASE_URL}${pathForFilmSimulation(simulation)}`; @@ -181,6 +184,9 @@ export const absolutePathForTagImage = (tag: string) => export const absolutePathForCameraImage= (camera: Camera) => `${absolutePathForCamera(camera)}/image`; +export const absolutePathForLensImage= (lens: Lens) => + `${absolutePathForLens(lens)}/image`; + export const absolutePathForFilmSimulationImage = (simulation: FilmSimulation) => `${absolutePathForFilmSimulation(simulation)}/image`; diff --git a/src/image-response/LensImageResponse.tsx b/src/image-response/LensImageResponse.tsx new file mode 100644 index 00000000..629f9794 --- /dev/null +++ b/src/image-response/LensImageResponse.tsx @@ -0,0 +1,47 @@ +import { Photo } from '../photo'; +import ImageCaption from './components/ImageCaption'; +import ImagePhotoGrid from './components/ImagePhotoGrid'; +import ImageContainer from './components/ImageContainer'; +import { NextImageSize } from '@/platforms/next-image'; +import { formatLensText, Lens, lensFromPhoto } from '@/lens'; +import { RiCameraLensLine } from 'react-icons/ri'; + +export default function LensImageResponse({ + lens: lensProp, + photos, + width, + height, + fontFamily, +}: { + lens: Lens + photos: Photo[] + width: NextImageSize + height: number + fontFamily: string +}) { + const lens = lensFromPhoto(photos[0], lensProp); + return ( + + + , + title: formatLensText(lens).toLocaleUpperCase(), + }} /> + + ); +} diff --git a/src/lens/LensHeader.tsx b/src/lens/LensHeader.tsx new file mode 100644 index 00000000..ddd388e6 --- /dev/null +++ b/src/lens/LensHeader.tsx @@ -0,0 +1,37 @@ +import { Photo, PhotoDateRange } from '@/photo'; +import PhotoHeader from '@/photo/PhotoHeader'; +import { Lens, lensFromPhoto } from '.'; +import PhotoLens from './PhotoLens'; +import { descriptionForLensPhotos } from './meta'; + +export default function LensHeader({ + lens: lensProp, + photos, + selectedPhoto, + indexNumber, + count, + dateRange, +}: { + lens: Lens + photos: Photo[] + selectedPhoto?: Photo + indexNumber?: number + count?: number + dateRange?: PhotoDateRange +}) { + const lens = lensFromPhoto(photos[0], lensProp); + return ( + } + entityDescription={ + descriptionForLensPhotos(photos, undefined, count, dateRange)} + photos={photos} + selectedPhoto={selectedPhoto} + indexNumber={indexNumber} + count={count} + dateRange={dateRange} + includeShareButton + /> + ); +} diff --git a/src/lens/LensOGTile.tsx b/src/lens/LensOGTile.tsx new file mode 100644 index 00000000..cf6e7263 --- /dev/null +++ b/src/lens/LensOGTile.tsx @@ -0,0 +1,43 @@ +import { Photo, PhotoDateRange } from '@/photo'; +import { absolutePathForLensImage, pathForLens } from '@/app/paths'; +import OGTile from '@/components/OGTile'; +import { Lens } from '.'; +import { titleForLens, descriptionForLensPhotos } from './meta'; + +export type OGLoadingState = 'unloaded' | 'loading' | 'loaded' | 'failed'; + +export default function LensOGTile({ + lens, + photos, + loadingState: loadingStateExternal, + riseOnHover, + onLoad, + onFail, + retryTime, + count, + dateRange, +}: { + lens: Lens + photos: Photo[] + loadingState?: OGLoadingState + onLoad?: () => void + onFail?: () => void + riseOnHover?: boolean + retryTime?: number + count?: number + dateRange?: PhotoDateRange +}) { + return ( + + ); +}; diff --git a/src/lens/LensOverview.tsx b/src/lens/LensOverview.tsx new file mode 100644 index 00000000..d8d2fc4a --- /dev/null +++ b/src/lens/LensOverview.tsx @@ -0,0 +1,34 @@ +import { Photo, PhotoDateRange } from '@/photo'; +import { Lens, createLensKey } from '.'; +import LensHeader from './LensHeader'; +import PhotoGridContainer from '@/photo/PhotoGridContainer'; + +export default function LensOverview({ + lens, + photos, + count, + dateRange, + animateOnFirstLoadOnly, +}: { + lens: Lens, + photos: Photo[], + count: number, + dateRange?: PhotoDateRange, + animateOnFirstLoadOnly?: boolean, +}) { + return ( + , + }} /> + ); +} diff --git a/src/lens/LensShareModal.tsx b/src/lens/LensShareModal.tsx new file mode 100644 index 00000000..bdf24372 --- /dev/null +++ b/src/lens/LensShareModal.tsx @@ -0,0 +1,24 @@ +import { absolutePathForLens } from '@/app/paths'; +import { PhotoSetAttributes } from '../photo/set'; +import ShareModal from '@/share/ShareModal'; +import { Lens } from '.'; +import { shareTextForLens } from './meta'; +import LensOGTile from './LensOGTile'; + +export default function LensShareModal({ + lens, + photos, + count, + dateRange, +}: { + lens: Lens +} & PhotoSetAttributes) { + return ( + + + + ); +}; diff --git a/src/lens/PhotoLens.tsx b/src/lens/PhotoLens.tsx index 4e75f897..b968c8ff 100644 --- a/src/lens/PhotoLens.tsx +++ b/src/lens/PhotoLens.tsx @@ -15,12 +15,11 @@ export default function PhotoLens({ className, }: { lens: Lens - hideAppleIcon?: boolean countOnHover?: number } & EntityLinkExternalProps) { return ( { + const lens = getLensFromParams({ make, model }); + return Promise.all([ + getPhotosCached({ lens, limit }), + getPhotosMetaCached({ lens }), + ]) + .then(([photos, meta]) => [ + photos, + meta, + lensFromPhoto(photos[0], lens), + ] as const); +}; diff --git a/src/lens/index.ts b/src/lens/index.ts index eb2a48ed..19c17dea 100644 --- a/src/lens/index.ts +++ b/src/lens/index.ts @@ -26,8 +26,9 @@ export type LensWithCount = { export type Lenses = LensWithCount[]; -export const createLensKey = ({ make, model }: Lens) => - parameterize(`${make}-${model}`, true); +// Support keys for make-only and model-only lens queries +export const createLensKey = ({ make, model }: Partial) => + parameterize(`${make ?? 'ANY'}-${model ?? 'ANY'}`, true); export const getLensFromParams = ({ make, @@ -54,5 +55,28 @@ const isLensMakeApple = (make?: string) => export const isLensApple = ({ make }: Lens) => isLensMakeApple(make); -export const formatLensText = ({ make, model }: Lens, short = true) => - short ? model : `${make} ${model}`; +export const formatLensText = ( + { make, model: modelRaw }: Lens, + length: + 'long' | // Unmodified make and model + 'medium' | // Make and model, with modifiers removed + 'short' // Model only + = 'medium', +) => { + // Capture simple make without modifiers like 'Corporation' or 'Company' + const makeSimple = make.match(/^(\S+)/)?.[1]; + const doesModelStartWithMake = ( + makeSimple && + modelRaw.toLocaleLowerCase().startsWith(makeSimple.toLocaleLowerCase()) + ); + const model = modelRaw; + switch (length) { + case 'long': + case 'medium': + return `${make} ${model}`; + case 'short': + return doesModelStartWithMake + ? model.replace(makeSimple, '').trim() + : model; + } +}; diff --git a/src/lens/meta.ts b/src/lens/meta.ts new file mode 100644 index 00000000..696cb87b --- /dev/null +++ b/src/lens/meta.ts @@ -0,0 +1,61 @@ +import { + Photo, + PhotoDateRange, + descriptionForPhotoSet, + photoQuantityText, +} from '@/photo'; +import { Lens, lensFromPhoto, formatLensText } from '.'; +import { + absolutePathForLens, + absolutePathForLensImage, +} from '@/app/paths'; + +// Meta functions moved to separate file to avoid +// dependencies (camelcase-keys) found in photo/index.ts +// which cause Jest to crash + +export const titleForLens = ( + lens: Lens, + photos: Photo[], + explicitCount?: number, +) => [ + 'Shot on', + formatLensText(lensFromPhoto(photos[0], lens)), + photoQuantityText(explicitCount ?? photos.length), +].join(' '); + +export const shareTextForLens = ( + lens: Lens, + photos: Photo[], +) => + [ + 'Photos shot on', + formatLensText(lensFromPhoto(photos[0], lens)), + ].join(' '); + +export const descriptionForLensPhotos = ( + photos: Photo[], + dateBased?: boolean, + explicitCount?: number, + explicitDateRange?: PhotoDateRange, +) => + descriptionForPhotoSet( + photos, + undefined, + dateBased, + explicitCount, + explicitDateRange, + ); + +export const generateMetaForLens = ( + lens: Lens, + photos: Photo[], + explicitCount?: number, + explicitDateRange?: PhotoDateRange, +) => ({ + url: absolutePathForLens(lens), + title: titleForLens(lens, photos, explicitCount), + description: + descriptionForLensPhotos(photos, true, explicitCount, explicitDateRange), + images: absolutePathForLensImage(lens), +}); diff --git a/src/photo/InfinitePhotoScroll.tsx b/src/photo/InfinitePhotoScroll.tsx index 96bbab98..1f165835 100644 --- a/src/photo/InfinitePhotoScroll.tsx +++ b/src/photo/InfinitePhotoScroll.tsx @@ -31,6 +31,7 @@ export default function InfinitePhotoScroll({ sortBy, tag, camera, + lens, simulation, wrapMoreButtonInGrid, useCachedPhotos = true, @@ -69,8 +70,9 @@ export default function InfinitePhotoScroll({ sortBy, limit: itemsPerPage, hidden: includeHiddenPhotos ? 'include' : 'exclude', - tag, camera, + lens, + tag, simulation, }, warmOnly) , [ @@ -79,8 +81,9 @@ export default function InfinitePhotoScroll({ initialOffset, itemsPerPage, includeHiddenPhotos, - tag, camera, + lens, + tag, simulation, ]); diff --git a/src/photo/PhotoDetailPage.tsx b/src/photo/PhotoDetailPage.tsx index 6e5636e5..64b453aa 100644 --- a/src/photo/PhotoDetailPage.tsx +++ b/src/photo/PhotoDetailPage.tsx @@ -13,6 +13,7 @@ import FocalLengthHeader from '@/focal/FocalLengthHeader'; import PhotoHeader from './PhotoHeader'; import RecipeHeader from '@/recipe/RecipeHeader'; import { ReactNode } from 'react'; +import LensHeader from '@/lens/LensHeader'; export default function PhotoDetailPage({ photo, @@ -20,6 +21,7 @@ export default function PhotoDetailPage({ photosGrid, tag, camera, + lens, simulation, recipe, focal, @@ -66,6 +68,15 @@ export default function PhotoDetailPage({ count={count} dateRange={dateRange} />; + } else if (lens) { + customHeader = ; } else if (simulation) { customHeader = + const renderPrevNext = ; - const renderDateRange = () => + const renderDateRange = {start === end ? start : <>{end}
– {start}}
; - const renderContentA = () => entity ?? ( + const renderContentA = entity ?? ( selectedPhoto !== undefined && {headerType === 'photo-detail-with-entity' - ? renderContentA() + ? renderContentA // Necessary for title truncation :

- {renderContentA()} + {renderContentA}

} {/* Content B: Filter Set Meta or Photo Pagination */} @@ -149,19 +141,15 @@ export default function PhotoHeader({ ? <> {entityDescription} {includeShareButton && - } + } : {entityVerb} {paginationLabel} @@ -176,8 +164,8 @@ export default function PhotoHeader({ 'justify-end', )}> {selectedPhoto - ? renderPrevNext() - : renderDateRange()} + ? renderPrevNext + : renderDateRange} , ]} diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index 30a83f2c..7e6bba7e 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -65,8 +65,9 @@ export default function PhotoLarge({ showZoomControls: showZoomControlsProp = true, shouldZoomOnFKeydown = true, shouldShare = true, - shouldShareTag, shouldShareCamera, + shouldShareLens, + shouldShareTag, shouldShareSimulation, shouldShareRecipe, shouldShareFocalLength, @@ -89,8 +90,9 @@ export default function PhotoLarge({ showZoomControls?: boolean shouldZoomOnFKeydown?: boolean shouldShare?: boolean - shouldShareTag?: boolean shouldShareCamera?: boolean + shouldShareLens?: boolean + shouldShareTag?: boolean shouldShareSimulation?: boolean shouldShareRecipe?: boolean shouldShareFocalLength?: boolean @@ -280,21 +282,20 @@ export default function PhotoLarge({ showTagsContent ) &&
- {showCameraContent && - } - {showLensContent && - <> -
- + - } + {showLensContent && + } +
} {showRecipeContent && recipeTitle && export const revalidateCamerasKey = () => revalidateTag(KEY_CAMERAS); +export const revalidateLensesKey = () => + revalidateTag(KEY_LENSES); + export const revalidateFilmSimulationsKey = () => revalidateTag(KEY_FILM_SIMULATIONS); @@ -115,6 +119,7 @@ export const revalidateAllKeys = () => { revalidatePhotosKey(); revalidateTagsKey(); revalidateCamerasKey(); + revalidateLensesKey(); revalidateFilmSimulationsKey(); revalidateRecipesKey(); revalidateFocalLengthsKey(); @@ -134,6 +139,7 @@ export const revalidatePhoto = (photoId: string) => { revalidateTag(photoId); revalidateTagsKey(); revalidateCamerasKey(); + revalidateLensesKey(); revalidateFilmSimulationsKey(); revalidateRecipesKey(); revalidateFocalLengthsKey(); @@ -144,6 +150,7 @@ export const revalidatePhoto = (photoId: string) => { revalidatePath(PATH_FEED, 'layout'); revalidatePath(PREFIX_TAG, 'layout'); revalidatePath(PREFIX_CAMERA, 'layout'); + revalidatePath(PREFIX_LENS, 'layout'); revalidatePath(PREFIX_FILM_SIMULATION, 'layout'); revalidatePath(PREFIX_RECIPE, 'layout'); revalidatePath(PREFIX_FOCAL_LENGTH, 'layout'); diff --git a/src/photo/db/index.ts b/src/photo/db/index.ts index 8b299eb2..1361fa96 100644 --- a/src/photo/db/index.ts +++ b/src/photo/db/index.ts @@ -2,6 +2,7 @@ import { PRIORITY_ORDER_ENABLED } from '@/app/config'; import { parameterize } from '@/utility/string'; import { PhotoSetCategory } from '../set'; import { Camera } from '@/camera'; +import { Lens } from '@/lens'; export const GENERATE_STATIC_PARAMS_LIMIT = 1000; export const PHOTO_DEFAULT_LIMIT = 100; @@ -23,8 +24,9 @@ export type GetPhotosOptions = { takenAfterInclusive?: Date updatedBefore?: Date hidden?: 'exclude' | 'include' | 'only' -} & Omit & { +} & Omit & { camera?: Partial + lens?: Partial }; export const areOptionsSensitive = (options: GetPhotosOptions) => @@ -83,10 +85,6 @@ export const getWheresFromOptions = ( wheres.push(`aspect_ratio <= $${valuesIndex++}`); wheresValues.push(maximumAspectRatio); } - if (tag) { - wheres.push(`$${valuesIndex++}=ANY(tags)`); - wheresValues.push(tag); - } if (camera?.make) { wheres.push(`${parameterizeForDb('make')}=$${valuesIndex++}`); wheresValues.push(parameterize(camera.make, true)); @@ -95,12 +93,18 @@ export const getWheresFromOptions = ( wheres.push(`${parameterizeForDb('model')}=$${valuesIndex++}`); wheresValues.push(parameterize(camera.model, true)); } - if (lens) { + if (lens?.make) { wheres.push(`${parameterizeForDb('lens_make')}=$${valuesIndex++}`); wheresValues.push(parameterize(lens.make, true)); + } + if (lens?.model) { wheres.push(`${parameterizeForDb('lens_model')}=$${valuesIndex++}`); wheresValues.push(parameterize(lens.model, true)); } + if (tag) { + wheres.push(`$${valuesIndex++}=ANY(tags)`); + wheresValues.push(tag); + } if (simulation) { wheres.push(`film_simulation=$${valuesIndex++}`); wheresValues.push(simulation); diff --git a/src/photo/db/query.ts b/src/photo/db/query.ts index 37837950..f27213fd 100644 --- a/src/photo/db/query.ts +++ b/src/photo/db/query.ts @@ -334,6 +334,24 @@ export const getUniqueCameras = async () => }))) , 'getUniqueCameras'); +export const getUniqueLenses = async () => + safelyQueryPhotos(() => sql` + SELECT DISTINCT lens_make||' '||lens_model as lens, + lens_make, lens_model, COUNT(*) + FROM photos + WHERE hidden IS NOT TRUE + AND trim(lens_make) <> '' + AND trim(lens_model) <> '' + GROUP BY lens_make, lens_model + ORDER BY lens ASC + `.then(({ rows }): Lenses => rows + .map(({ lens_make: make, lens_model: model, count }) => ({ + lensKey: createLensKey({ make, model }), + lens: { make, model }, + count: parseInt(count, 10), + }))) + , 'getUniqueLenses'); + export const getUniqueRecipes = async () => safelyQueryPhotos(() => sql` SELECT DISTINCT recipe_title, COUNT(*) @@ -405,24 +423,6 @@ export const getUniqueFilmSimulations = async () => }))) , 'getUniqueFilmSimulations'); -export const getUniqueLenses = async () => - safelyQueryPhotos(() => sql` - SELECT DISTINCT lens_make||' '||lens_model as lens, - lens_make, lens_model, COUNT(*) - FROM photos - WHERE hidden IS NOT TRUE - AND trim(lens_make) <> '' - AND trim(lens_model) <> '' - GROUP BY lens_make, lens_model - ORDER BY lens ASC - `.then(({ rows }): Lenses => rows - .map(({ lens_make: make, lens_model: model, count }) => ({ - lensKey: createLensKey({ make, model }), - lens: { make, model }, - count: parseInt(count, 10), - }))) - , 'getUniqueLenses'); - export const getUniqueFocalLengths = async () => safelyQueryPhotos(() => sql` SELECT DISTINCT focal_length, COUNT(*) diff --git a/src/photo/set.ts b/src/photo/set.ts index 2d9076c4..0df49d1a 100644 --- a/src/photo/set.ts +++ b/src/photo/set.ts @@ -31,12 +31,12 @@ export const getHiddenDefaultCategories = (keys: CategoryKeys): CategoryKeys => DEFAULT_CATEGORY_KEYS.filter(key => !keys.includes(key)); export interface PhotoSetCategory { - tag?: string camera?: Camera + lens?: Lens + tag?: string recipe?: string simulation?: FilmSimulation focal?: number - lens?: Lens // Unimplemented as a set } export interface PhotoSetCategories { diff --git a/src/share/ShareModals.tsx b/src/share/ShareModals.tsx index 21644e70..9e1ebd51 100644 --- a/src/share/ShareModals.tsx +++ b/src/share/ShareModals.tsx @@ -7,6 +7,7 @@ import FilmSimulationShareModal from '@/simulation/FilmSimulationShareModal'; import FocalLengthShareModal from '@/focal/FocalLengthShareModal'; import { useAppState } from '@/state/AppState'; import RecipeShareModal from '@/recipe/RecipeShareModal'; +import LensShareModal from '@/lens/LensShareModal'; export default function ShareModals() { const { shareModalProps = {} } = useAppState(); @@ -16,8 +17,9 @@ export default function ShareModals() { photos, count, dateRange, - tag, camera, + lens, + tag, simulation, recipe, focal, @@ -38,6 +40,8 @@ export default function ShareModals() { return ; } else if (camera) { return ; + } else if (lens) { + return ; } else if (simulation) { return ; } else if (recipe) { diff --git a/src/share/index.ts b/src/share/index.ts index 5929814d..86a7f305 100644 --- a/src/share/index.ts +++ b/src/share/index.ts @@ -4,7 +4,9 @@ import { absolutePathForCameraImage, absolutePathForFilmSimulationImage, absolutePathForFocalLengthImage, + absolutePathForLensImage, absolutePathForPhotoImage, + absolutePathForRecipeImage, absolutePathForTagImage, } from '@/app/paths'; @@ -15,24 +17,26 @@ export type ShareModalProps = Omit & { export const getSharePathFromShareModalProps = ({ photo, - tag, camera, + lens, + tag, + recipe, simulation, focal, }: ShareModalProps) => { if (photo) { return absolutePathForPhotoImage(photo); - } - if (tag) { - return absolutePathForTagImage(tag); - } - if (camera) { + } else if (camera) { return absolutePathForCameraImage(camera); - } - if (simulation) { + } else if (lens) { + return absolutePathForLensImage(lens); + } else if (tag) { + return absolutePathForTagImage(tag); + } else if (recipe) { + return absolutePathForRecipeImage(recipe); + } else if (simulation) { return absolutePathForFilmSimulationImage(simulation); - } - if (focal) { + } else if (focal) { return absolutePathForFocalLengthImage(focal); } };