Create core lens pages
This commit is contained in:
parent
bb2c8dddc6
commit
ee265f1f33
93
app/lens/[make]/[model]/[photoId]/page.tsx
Normal file
93
app/lens/[make]/[model]/[photoId]/page.tsx
Normal file
@ -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<Metadata> {
|
||||||
|
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 (
|
||||||
|
<PhotoDetailPage {...{
|
||||||
|
photo,
|
||||||
|
photos,
|
||||||
|
photosGrid,
|
||||||
|
lens,
|
||||||
|
indexNumber,
|
||||||
|
count,
|
||||||
|
dateRange,
|
||||||
|
}} />
|
||||||
|
);
|
||||||
|
}
|
||||||
61
app/lens/[make]/[model]/image/route.tsx
Normal file
61
app/lens/[make]/[model]/image/route.tsx
Normal file
@ -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(
|
||||||
|
<LensImageResponse {...{
|
||||||
|
lens,
|
||||||
|
photos,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fontFamily,
|
||||||
|
}}/>,
|
||||||
|
{ width, height, fonts, headers },
|
||||||
|
);
|
||||||
|
}
|
||||||
80
app/lens/[make]/[model]/page.tsx
Normal file
80
app/lens/[make]/[model]/page.tsx
Normal file
@ -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<Metadata> {
|
||||||
|
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 (
|
||||||
|
<LensOverview {...{ lens, photos, count, dateRange }} />
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -163,6 +163,9 @@ export const absolutePathForTag = (tag: string) =>
|
|||||||
export const absolutePathForCamera= (camera: Camera) =>
|
export const absolutePathForCamera= (camera: Camera) =>
|
||||||
`${BASE_URL}${pathForCamera(camera)}`;
|
`${BASE_URL}${pathForCamera(camera)}`;
|
||||||
|
|
||||||
|
export const absolutePathForLens= (lens: Lens) =>
|
||||||
|
`${BASE_URL}${pathForLens(lens)}`;
|
||||||
|
|
||||||
export const absolutePathForFilmSimulation = (simulation: FilmSimulation) =>
|
export const absolutePathForFilmSimulation = (simulation: FilmSimulation) =>
|
||||||
`${BASE_URL}${pathForFilmSimulation(simulation)}`;
|
`${BASE_URL}${pathForFilmSimulation(simulation)}`;
|
||||||
|
|
||||||
@ -181,6 +184,9 @@ export const absolutePathForTagImage = (tag: string) =>
|
|||||||
export const absolutePathForCameraImage= (camera: Camera) =>
|
export const absolutePathForCameraImage= (camera: Camera) =>
|
||||||
`${absolutePathForCamera(camera)}/image`;
|
`${absolutePathForCamera(camera)}/image`;
|
||||||
|
|
||||||
|
export const absolutePathForLensImage= (lens: Lens) =>
|
||||||
|
`${absolutePathForLens(lens)}/image`;
|
||||||
|
|
||||||
export const absolutePathForFilmSimulationImage =
|
export const absolutePathForFilmSimulationImage =
|
||||||
(simulation: FilmSimulation) =>
|
(simulation: FilmSimulation) =>
|
||||||
`${absolutePathForFilmSimulation(simulation)}/image`;
|
`${absolutePathForFilmSimulation(simulation)}/image`;
|
||||||
|
|||||||
47
src/image-response/LensImageResponse.tsx
Normal file
47
src/image-response/LensImageResponse.tsx
Normal file
@ -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 (
|
||||||
|
<ImageContainer solidBackground={photos.length === 0}>
|
||||||
|
<ImagePhotoGrid
|
||||||
|
{...{
|
||||||
|
photos,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ImageCaption {...{
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fontFamily,
|
||||||
|
icon: <RiCameraLensLine
|
||||||
|
size={height * .079}
|
||||||
|
style={{
|
||||||
|
marginRight: height * .015,
|
||||||
|
marginTop: height * .003,
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
title: formatLensText(lens).toLocaleUpperCase(),
|
||||||
|
}} />
|
||||||
|
</ImageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
37
src/lens/LensHeader.tsx
Normal file
37
src/lens/LensHeader.tsx
Normal file
@ -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 (
|
||||||
|
<PhotoHeader
|
||||||
|
lens={lens}
|
||||||
|
entity={<PhotoLens {...{ lens }} contrast="high" />}
|
||||||
|
entityDescription={
|
||||||
|
descriptionForLensPhotos(photos, undefined, count, dateRange)}
|
||||||
|
photos={photos}
|
||||||
|
selectedPhoto={selectedPhoto}
|
||||||
|
indexNumber={indexNumber}
|
||||||
|
count={count}
|
||||||
|
dateRange={dateRange}
|
||||||
|
includeShareButton
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
43
src/lens/LensOGTile.tsx
Normal file
43
src/lens/LensOGTile.tsx
Normal file
@ -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 (
|
||||||
|
<OGTile {...{
|
||||||
|
title: titleForLens(lens, photos, count),
|
||||||
|
description: descriptionForLensPhotos(photos, true, count, dateRange),
|
||||||
|
path: pathForLens(lens),
|
||||||
|
pathImageAbsolute: absolutePathForLensImage(lens),
|
||||||
|
loadingState: loadingStateExternal,
|
||||||
|
onLoad,
|
||||||
|
onFail,
|
||||||
|
riseOnHover,
|
||||||
|
retryTime,
|
||||||
|
}}/>
|
||||||
|
);
|
||||||
|
};
|
||||||
34
src/lens/LensOverview.tsx
Normal file
34
src/lens/LensOverview.tsx
Normal file
@ -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 (
|
||||||
|
<PhotoGridContainer {...{
|
||||||
|
cacheKey: `lens-${createLensKey(lens)}`,
|
||||||
|
photos,
|
||||||
|
count,
|
||||||
|
lens,
|
||||||
|
animateOnFirstLoadOnly,
|
||||||
|
header: <LensHeader {...{
|
||||||
|
lens,
|
||||||
|
photos,
|
||||||
|
count,
|
||||||
|
dateRange,
|
||||||
|
}} />,
|
||||||
|
}} />
|
||||||
|
);
|
||||||
|
}
|
||||||
24
src/lens/LensShareModal.tsx
Normal file
24
src/lens/LensShareModal.tsx
Normal file
@ -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 (
|
||||||
|
<ShareModal
|
||||||
|
pathShare={absolutePathForLens(lens)}
|
||||||
|
socialText={shareTextForLens(lens, photos)}
|
||||||
|
>
|
||||||
|
<LensOGTile {...{ lens, photos, count, dateRange }} />
|
||||||
|
</ShareModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -15,12 +15,11 @@ export default function PhotoLens({
|
|||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
lens: Lens
|
lens: Lens
|
||||||
hideAppleIcon?: boolean
|
|
||||||
countOnHover?: number
|
countOnHover?: number
|
||||||
} & EntityLinkExternalProps) {
|
} & EntityLinkExternalProps) {
|
||||||
return (
|
return (
|
||||||
<EntityLink
|
<EntityLink
|
||||||
label={formatLensText(lens)}
|
label={formatLensText(lens, 'short')}
|
||||||
href={pathForLens(lens)}
|
href={pathForLens(lens)}
|
||||||
icon={<RiCameraLensLine
|
icon={<RiCameraLensLine
|
||||||
size={14}
|
size={14}
|
||||||
|
|||||||
22
src/lens/data.ts
Normal file
22
src/lens/data.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { getLensFromParams, lensFromPhoto } from '.';
|
||||||
|
import {
|
||||||
|
getPhotosCached,
|
||||||
|
getPhotosMetaCached,
|
||||||
|
} from '@/photo/cache';
|
||||||
|
|
||||||
|
export const getPhotosLensDataCached = async (
|
||||||
|
make: string,
|
||||||
|
model: string,
|
||||||
|
limit: number,
|
||||||
|
) => {
|
||||||
|
const lens = getLensFromParams({ make, model });
|
||||||
|
return Promise.all([
|
||||||
|
getPhotosCached({ lens, limit }),
|
||||||
|
getPhotosMetaCached({ lens }),
|
||||||
|
])
|
||||||
|
.then(([photos, meta]) => [
|
||||||
|
photos,
|
||||||
|
meta,
|
||||||
|
lensFromPhoto(photos[0], lens),
|
||||||
|
] as const);
|
||||||
|
};
|
||||||
@ -26,8 +26,9 @@ export type LensWithCount = {
|
|||||||
|
|
||||||
export type Lenses = LensWithCount[];
|
export type Lenses = LensWithCount[];
|
||||||
|
|
||||||
export const createLensKey = ({ make, model }: Lens) =>
|
// Support keys for make-only and model-only lens queries
|
||||||
parameterize(`${make}-${model}`, true);
|
export const createLensKey = ({ make, model }: Partial<Lens>) =>
|
||||||
|
parameterize(`${make ?? 'ANY'}-${model ?? 'ANY'}`, true);
|
||||||
|
|
||||||
export const getLensFromParams = ({
|
export const getLensFromParams = ({
|
||||||
make,
|
make,
|
||||||
@ -54,5 +55,28 @@ const isLensMakeApple = (make?: string) =>
|
|||||||
export const isLensApple = ({ make }: Lens) =>
|
export const isLensApple = ({ make }: Lens) =>
|
||||||
isLensMakeApple(make);
|
isLensMakeApple(make);
|
||||||
|
|
||||||
export const formatLensText = ({ make, model }: Lens, short = true) =>
|
export const formatLensText = (
|
||||||
short ? model : `${make} ${model}`;
|
{ 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
61
src/lens/meta.ts
Normal file
61
src/lens/meta.ts
Normal file
@ -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),
|
||||||
|
});
|
||||||
@ -31,6 +31,7 @@ export default function InfinitePhotoScroll({
|
|||||||
sortBy,
|
sortBy,
|
||||||
tag,
|
tag,
|
||||||
camera,
|
camera,
|
||||||
|
lens,
|
||||||
simulation,
|
simulation,
|
||||||
wrapMoreButtonInGrid,
|
wrapMoreButtonInGrid,
|
||||||
useCachedPhotos = true,
|
useCachedPhotos = true,
|
||||||
@ -69,8 +70,9 @@ export default function InfinitePhotoScroll({
|
|||||||
sortBy,
|
sortBy,
|
||||||
limit: itemsPerPage,
|
limit: itemsPerPage,
|
||||||
hidden: includeHiddenPhotos ? 'include' : 'exclude',
|
hidden: includeHiddenPhotos ? 'include' : 'exclude',
|
||||||
tag,
|
|
||||||
camera,
|
camera,
|
||||||
|
lens,
|
||||||
|
tag,
|
||||||
simulation,
|
simulation,
|
||||||
}, warmOnly)
|
}, warmOnly)
|
||||||
, [
|
, [
|
||||||
@ -79,8 +81,9 @@ export default function InfinitePhotoScroll({
|
|||||||
initialOffset,
|
initialOffset,
|
||||||
itemsPerPage,
|
itemsPerPage,
|
||||||
includeHiddenPhotos,
|
includeHiddenPhotos,
|
||||||
tag,
|
|
||||||
camera,
|
camera,
|
||||||
|
lens,
|
||||||
|
tag,
|
||||||
simulation,
|
simulation,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import FocalLengthHeader from '@/focal/FocalLengthHeader';
|
|||||||
import PhotoHeader from './PhotoHeader';
|
import PhotoHeader from './PhotoHeader';
|
||||||
import RecipeHeader from '@/recipe/RecipeHeader';
|
import RecipeHeader from '@/recipe/RecipeHeader';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
import LensHeader from '@/lens/LensHeader';
|
||||||
|
|
||||||
export default function PhotoDetailPage({
|
export default function PhotoDetailPage({
|
||||||
photo,
|
photo,
|
||||||
@ -20,6 +21,7 @@ export default function PhotoDetailPage({
|
|||||||
photosGrid,
|
photosGrid,
|
||||||
tag,
|
tag,
|
||||||
camera,
|
camera,
|
||||||
|
lens,
|
||||||
simulation,
|
simulation,
|
||||||
recipe,
|
recipe,
|
||||||
focal,
|
focal,
|
||||||
@ -66,6 +68,15 @@ export default function PhotoDetailPage({
|
|||||||
count={count}
|
count={count}
|
||||||
dateRange={dateRange}
|
dateRange={dateRange}
|
||||||
/>;
|
/>;
|
||||||
|
} else if (lens) {
|
||||||
|
customHeader = <LensHeader
|
||||||
|
lens={lens}
|
||||||
|
photos={photos}
|
||||||
|
selectedPhoto={photo}
|
||||||
|
indexNumber={indexNumber}
|
||||||
|
count={count}
|
||||||
|
dateRange={dateRange}
|
||||||
|
/>;
|
||||||
} else if (simulation) {
|
} else if (simulation) {
|
||||||
customHeader = <FilmSimulationHeader
|
customHeader = <FilmSimulationHeader
|
||||||
simulation={simulation}
|
simulation={simulation}
|
||||||
@ -117,11 +128,13 @@ export default function PhotoDetailPage({
|
|||||||
showTitle={Boolean(customHeader)}
|
showTitle={Boolean(customHeader)}
|
||||||
showTitleAsH1
|
showTitleAsH1
|
||||||
showCamera={!camera}
|
showCamera={!camera}
|
||||||
|
showLens={!lens}
|
||||||
showSimulation={!simulation}
|
showSimulation={!simulation}
|
||||||
showRecipe={!recipe}
|
showRecipe={!recipe}
|
||||||
shouldShare={shouldShare}
|
shouldShare={shouldShare}
|
||||||
shouldShareTag={tag !== undefined}
|
|
||||||
shouldShareCamera={camera !== undefined}
|
shouldShareCamera={camera !== undefined}
|
||||||
|
shouldShareLens={lens !== undefined}
|
||||||
|
shouldShareTag={tag !== undefined}
|
||||||
shouldShareSimulation={simulation !== undefined}
|
shouldShareSimulation={simulation !== undefined}
|
||||||
shouldShareRecipe={recipe !== undefined}
|
shouldShareRecipe={recipe !== undefined}
|
||||||
shouldShareFocalLength={focal !== undefined}
|
shouldShareFocalLength={focal !== undefined}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ export default function PhotoGrid({
|
|||||||
selectedPhoto,
|
selectedPhoto,
|
||||||
tag,
|
tag,
|
||||||
camera,
|
camera,
|
||||||
|
lens,
|
||||||
simulation,
|
simulation,
|
||||||
focal,
|
focal,
|
||||||
recipe,
|
recipe,
|
||||||
@ -96,6 +97,7 @@ export default function PhotoGrid({
|
|||||||
photo,
|
photo,
|
||||||
tag,
|
tag,
|
||||||
camera,
|
camera,
|
||||||
|
lens,
|
||||||
simulation,
|
simulation,
|
||||||
focal,
|
focal,
|
||||||
recipe,
|
recipe,
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export default function PhotoGridContainer({
|
|||||||
count,
|
count,
|
||||||
tag,
|
tag,
|
||||||
camera,
|
camera,
|
||||||
|
lens,
|
||||||
simulation,
|
simulation,
|
||||||
focal,
|
focal,
|
||||||
recipe,
|
recipe,
|
||||||
@ -51,6 +52,7 @@ export default function PhotoGridContainer({
|
|||||||
photos,
|
photos,
|
||||||
tag,
|
tag,
|
||||||
camera,
|
camera,
|
||||||
|
lens,
|
||||||
simulation,
|
simulation,
|
||||||
focal,
|
focal,
|
||||||
recipe,
|
recipe,
|
||||||
@ -65,6 +67,7 @@ export default function PhotoGridContainer({
|
|||||||
canStart: shouldAnimateDynamicItems,
|
canStart: shouldAnimateDynamicItems,
|
||||||
tag,
|
tag,
|
||||||
camera,
|
camera,
|
||||||
|
lens,
|
||||||
simulation,
|
simulation,
|
||||||
focal,
|
focal,
|
||||||
recipe,
|
recipe,
|
||||||
|
|||||||
@ -19,11 +19,6 @@ import { useAppState } from '@/state/AppState';
|
|||||||
import { GRID_GAP_CLASSNAME } from '@/components';
|
import { GRID_GAP_CLASSNAME } from '@/components';
|
||||||
|
|
||||||
export default function PhotoHeader({
|
export default function PhotoHeader({
|
||||||
tag,
|
|
||||||
camera,
|
|
||||||
simulation,
|
|
||||||
focal,
|
|
||||||
recipe,
|
|
||||||
photos,
|
photos,
|
||||||
selectedPhoto,
|
selectedPhoto,
|
||||||
entity,
|
entity,
|
||||||
@ -33,6 +28,7 @@ export default function PhotoHeader({
|
|||||||
count,
|
count,
|
||||||
dateRange,
|
dateRange,
|
||||||
includeShareButton,
|
includeShareButton,
|
||||||
|
...categories
|
||||||
}: {
|
}: {
|
||||||
photos: Photo[]
|
photos: Photo[]
|
||||||
selectedPhoto?: Photo
|
selectedPhoto?: Photo
|
||||||
@ -62,25 +58,21 @@ export default function PhotoHeader({
|
|||||||
? 'photo-detail-with-entity'
|
? 'photo-detail-with-entity'
|
||||||
: 'photo-detail';
|
: 'photo-detail';
|
||||||
|
|
||||||
const renderPrevNext = () =>
|
const renderPrevNext =
|
||||||
<PhotoPrevNext {...{
|
<PhotoPrevNext {...{
|
||||||
photo: selectedPhoto,
|
photo: selectedPhoto,
|
||||||
photos,
|
photos,
|
||||||
tag,
|
...categories,
|
||||||
camera,
|
|
||||||
simulation,
|
|
||||||
focal,
|
|
||||||
recipe,
|
|
||||||
}} />;
|
}} />;
|
||||||
|
|
||||||
const renderDateRange = () =>
|
const renderDateRange =
|
||||||
<span className="text-dim uppercase text-right">
|
<span className="text-dim uppercase text-right">
|
||||||
{start === end
|
{start === end
|
||||||
? start
|
? start
|
||||||
: <>{end}<br />– {start}</>}
|
: <>{end}<br />– {start}</>}
|
||||||
</span>;
|
</span>;
|
||||||
|
|
||||||
const renderContentA = () => entity ?? (
|
const renderContentA = entity ?? (
|
||||||
selectedPhoto !== undefined &&
|
selectedPhoto !== undefined &&
|
||||||
<PhotoLink
|
<PhotoLink
|
||||||
photo={selectedPhoto}
|
photo={selectedPhoto}
|
||||||
@ -121,13 +113,13 @@ export default function PhotoHeader({
|
|||||||
: 'col-span-3 md:col-span-2 lg:col-span-3 w-[110%] xl:w-full',
|
: 'col-span-3 md:col-span-2 lg:col-span-3 w-[110%] xl:w-full',
|
||||||
)}>
|
)}>
|
||||||
{headerType === 'photo-detail-with-entity'
|
{headerType === 'photo-detail-with-entity'
|
||||||
? renderContentA()
|
? renderContentA
|
||||||
// Necessary for title truncation
|
// Necessary for title truncation
|
||||||
: <h1 className={clsx(
|
: <h1 className={clsx(
|
||||||
'w-full truncate',
|
'w-full truncate',
|
||||||
headerType !== 'photo-detail' && 'pr-1 sm:pr-2',
|
headerType !== 'photo-detail' && 'pr-1 sm:pr-2',
|
||||||
)}>
|
)}>
|
||||||
{renderContentA()}
|
{renderContentA}
|
||||||
</h1>}
|
</h1>}
|
||||||
</div>
|
</div>
|
||||||
{/* Content B: Filter Set Meta or Photo Pagination */}
|
{/* Content B: Filter Set Meta or Photo Pagination */}
|
||||||
@ -149,19 +141,15 @@ export default function PhotoHeader({
|
|||||||
? <>
|
? <>
|
||||||
{entityDescription}
|
{entityDescription}
|
||||||
{includeShareButton &&
|
{includeShareButton &&
|
||||||
<ShareButton
|
<ShareButton {...{
|
||||||
photos={photos}
|
photos,
|
||||||
tag={tag}
|
...categories,
|
||||||
camera={camera}
|
count,
|
||||||
simulation={simulation}
|
dateRange,
|
||||||
recipe={recipe}
|
className: 'translate-y-[1.5px]',
|
||||||
focal={focal}
|
prefetch: true,
|
||||||
count={count}
|
dim: true,
|
||||||
dateRange={dateRange}
|
}} />}
|
||||||
className="translate-y-[1.5px]"
|
|
||||||
prefetch
|
|
||||||
dim
|
|
||||||
/>}
|
|
||||||
</>
|
</>
|
||||||
: <ResponsiveText shortText={paginationLabel}>
|
: <ResponsiveText shortText={paginationLabel}>
|
||||||
{entityVerb} {paginationLabel}
|
{entityVerb} {paginationLabel}
|
||||||
@ -176,8 +164,8 @@ export default function PhotoHeader({
|
|||||||
'justify-end',
|
'justify-end',
|
||||||
)}>
|
)}>
|
||||||
{selectedPhoto
|
{selectedPhoto
|
||||||
? renderPrevNext()
|
? renderPrevNext
|
||||||
: renderDateRange()}
|
: renderDateRange}
|
||||||
</div>
|
</div>
|
||||||
</DivDebugBaselineGrid>,
|
</DivDebugBaselineGrid>,
|
||||||
]}
|
]}
|
||||||
|
|||||||
@ -65,8 +65,9 @@ export default function PhotoLarge({
|
|||||||
showZoomControls: showZoomControlsProp = true,
|
showZoomControls: showZoomControlsProp = true,
|
||||||
shouldZoomOnFKeydown = true,
|
shouldZoomOnFKeydown = true,
|
||||||
shouldShare = true,
|
shouldShare = true,
|
||||||
shouldShareTag,
|
|
||||||
shouldShareCamera,
|
shouldShareCamera,
|
||||||
|
shouldShareLens,
|
||||||
|
shouldShareTag,
|
||||||
shouldShareSimulation,
|
shouldShareSimulation,
|
||||||
shouldShareRecipe,
|
shouldShareRecipe,
|
||||||
shouldShareFocalLength,
|
shouldShareFocalLength,
|
||||||
@ -89,8 +90,9 @@ export default function PhotoLarge({
|
|||||||
showZoomControls?: boolean
|
showZoomControls?: boolean
|
||||||
shouldZoomOnFKeydown?: boolean
|
shouldZoomOnFKeydown?: boolean
|
||||||
shouldShare?: boolean
|
shouldShare?: boolean
|
||||||
shouldShareTag?: boolean
|
|
||||||
shouldShareCamera?: boolean
|
shouldShareCamera?: boolean
|
||||||
|
shouldShareLens?: boolean
|
||||||
|
shouldShareTag?: boolean
|
||||||
shouldShareSimulation?: boolean
|
shouldShareSimulation?: boolean
|
||||||
shouldShareRecipe?: boolean
|
shouldShareRecipe?: boolean
|
||||||
shouldShareFocalLength?: boolean
|
shouldShareFocalLength?: boolean
|
||||||
@ -280,21 +282,20 @@ export default function PhotoLarge({
|
|||||||
showTagsContent
|
showTagsContent
|
||||||
) &&
|
) &&
|
||||||
<div>
|
<div>
|
||||||
{showCameraContent &&
|
{(showCameraContent || showLensContent) &&
|
||||||
<PhotoCamera
|
<div className="flex flex-col">
|
||||||
camera={camera}
|
<PhotoCamera
|
||||||
contrast="medium"
|
camera={camera}
|
||||||
prefetch={prefetchRelatedLinks}
|
|
||||||
/>}
|
|
||||||
{showLensContent &&
|
|
||||||
<>
|
|
||||||
<br />
|
|
||||||
<PhotoLens
|
|
||||||
lens={lens}
|
|
||||||
contrast="medium"
|
contrast="medium"
|
||||||
prefetch={prefetchRelatedLinks}
|
prefetch={prefetchRelatedLinks}
|
||||||
/>
|
/>
|
||||||
</>}
|
{showLensContent &&
|
||||||
|
<PhotoLens
|
||||||
|
lens={lens}
|
||||||
|
contrast="medium"
|
||||||
|
prefetch={prefetchRelatedLinks}
|
||||||
|
/>}
|
||||||
|
</div>}
|
||||||
{showRecipeContent && recipeTitle &&
|
{showRecipeContent && recipeTitle &&
|
||||||
<PhotoRecipe
|
<PhotoRecipe
|
||||||
recipe={recipeTitle}
|
recipe={recipeTitle}
|
||||||
@ -426,6 +427,7 @@ export default function PhotoLarge({
|
|||||||
photo={photo}
|
photo={photo}
|
||||||
tag={shouldShareTag ? primaryTag : undefined}
|
tag={shouldShareTag ? primaryTag : undefined}
|
||||||
camera={shouldShareCamera ? camera : undefined}
|
camera={shouldShareCamera ? camera : undefined}
|
||||||
|
lens={shouldShareLens ? lens : undefined}
|
||||||
simulation={shouldShareSimulation
|
simulation={shouldShareSimulation
|
||||||
? photo.filmSimulation
|
? photo.filmSimulation
|
||||||
: undefined}
|
: undefined}
|
||||||
|
|||||||
@ -17,16 +17,12 @@ import Spinner from '@/components/Spinner';
|
|||||||
|
|
||||||
export default function PhotoMedium({
|
export default function PhotoMedium({
|
||||||
photo,
|
photo,
|
||||||
tag,
|
|
||||||
camera,
|
|
||||||
simulation,
|
|
||||||
focal,
|
|
||||||
recipe,
|
|
||||||
selected,
|
selected,
|
||||||
priority,
|
priority,
|
||||||
prefetch = SHOULD_PREFETCH_ALL_LINKS,
|
prefetch = SHOULD_PREFETCH_ALL_LINKS,
|
||||||
className,
|
className,
|
||||||
onVisible,
|
onVisible,
|
||||||
|
...categories
|
||||||
}: {
|
}: {
|
||||||
photo: Photo
|
photo: Photo
|
||||||
selected?: boolean
|
selected?: boolean
|
||||||
@ -42,7 +38,7 @@ export default function PhotoMedium({
|
|||||||
return (
|
return (
|
||||||
<LinkWithStatus
|
<LinkWithStatus
|
||||||
ref={ref}
|
ref={ref}
|
||||||
href={pathForPhoto({ photo, tag, camera, simulation, focal, recipe })}
|
href={pathForPhoto({ photo, ...categories })}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'active:brightness-75',
|
'active:brightness-75',
|
||||||
selected && 'brightness-50',
|
selected && 'brightness-50',
|
||||||
|
|||||||
@ -14,14 +14,11 @@ import useVisible from '@/utility/useVisible';
|
|||||||
|
|
||||||
export default function PhotoSmall({
|
export default function PhotoSmall({
|
||||||
photo,
|
photo,
|
||||||
tag,
|
|
||||||
camera,
|
|
||||||
simulation,
|
|
||||||
focal,
|
|
||||||
selected,
|
selected,
|
||||||
className,
|
className,
|
||||||
prefetch = SHOULD_PREFETCH_ALL_LINKS,
|
prefetch = SHOULD_PREFETCH_ALL_LINKS,
|
||||||
onVisible,
|
onVisible,
|
||||||
|
...categories
|
||||||
}: {
|
}: {
|
||||||
photo: Photo
|
photo: Photo
|
||||||
selected?: boolean
|
selected?: boolean
|
||||||
@ -36,7 +33,7 @@ export default function PhotoSmall({
|
|||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
ref={ref}
|
ref={ref}
|
||||||
href={pathForPhoto({ photo, tag, camera, simulation, focal })}
|
href={pathForPhoto({ photo, ...categories })}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
className,
|
className,
|
||||||
'active:brightness-75',
|
'active:brightness-75',
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import {
|
|||||||
PREFIX_CAMERA,
|
PREFIX_CAMERA,
|
||||||
PREFIX_FILM_SIMULATION,
|
PREFIX_FILM_SIMULATION,
|
||||||
PREFIX_FOCAL_LENGTH,
|
PREFIX_FOCAL_LENGTH,
|
||||||
|
PREFIX_LENS,
|
||||||
PREFIX_RECIPE,
|
PREFIX_RECIPE,
|
||||||
PREFIX_TAG,
|
PREFIX_TAG,
|
||||||
pathForPhoto,
|
pathForPhoto,
|
||||||
@ -105,6 +106,9 @@ export const revalidateRecipesKey = () =>
|
|||||||
export const revalidateCamerasKey = () =>
|
export const revalidateCamerasKey = () =>
|
||||||
revalidateTag(KEY_CAMERAS);
|
revalidateTag(KEY_CAMERAS);
|
||||||
|
|
||||||
|
export const revalidateLensesKey = () =>
|
||||||
|
revalidateTag(KEY_LENSES);
|
||||||
|
|
||||||
export const revalidateFilmSimulationsKey = () =>
|
export const revalidateFilmSimulationsKey = () =>
|
||||||
revalidateTag(KEY_FILM_SIMULATIONS);
|
revalidateTag(KEY_FILM_SIMULATIONS);
|
||||||
|
|
||||||
@ -115,6 +119,7 @@ export const revalidateAllKeys = () => {
|
|||||||
revalidatePhotosKey();
|
revalidatePhotosKey();
|
||||||
revalidateTagsKey();
|
revalidateTagsKey();
|
||||||
revalidateCamerasKey();
|
revalidateCamerasKey();
|
||||||
|
revalidateLensesKey();
|
||||||
revalidateFilmSimulationsKey();
|
revalidateFilmSimulationsKey();
|
||||||
revalidateRecipesKey();
|
revalidateRecipesKey();
|
||||||
revalidateFocalLengthsKey();
|
revalidateFocalLengthsKey();
|
||||||
@ -134,6 +139,7 @@ export const revalidatePhoto = (photoId: string) => {
|
|||||||
revalidateTag(photoId);
|
revalidateTag(photoId);
|
||||||
revalidateTagsKey();
|
revalidateTagsKey();
|
||||||
revalidateCamerasKey();
|
revalidateCamerasKey();
|
||||||
|
revalidateLensesKey();
|
||||||
revalidateFilmSimulationsKey();
|
revalidateFilmSimulationsKey();
|
||||||
revalidateRecipesKey();
|
revalidateRecipesKey();
|
||||||
revalidateFocalLengthsKey();
|
revalidateFocalLengthsKey();
|
||||||
@ -144,6 +150,7 @@ export const revalidatePhoto = (photoId: string) => {
|
|||||||
revalidatePath(PATH_FEED, 'layout');
|
revalidatePath(PATH_FEED, 'layout');
|
||||||
revalidatePath(PREFIX_TAG, 'layout');
|
revalidatePath(PREFIX_TAG, 'layout');
|
||||||
revalidatePath(PREFIX_CAMERA, 'layout');
|
revalidatePath(PREFIX_CAMERA, 'layout');
|
||||||
|
revalidatePath(PREFIX_LENS, 'layout');
|
||||||
revalidatePath(PREFIX_FILM_SIMULATION, 'layout');
|
revalidatePath(PREFIX_FILM_SIMULATION, 'layout');
|
||||||
revalidatePath(PREFIX_RECIPE, 'layout');
|
revalidatePath(PREFIX_RECIPE, 'layout');
|
||||||
revalidatePath(PREFIX_FOCAL_LENGTH, 'layout');
|
revalidatePath(PREFIX_FOCAL_LENGTH, 'layout');
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { PRIORITY_ORDER_ENABLED } from '@/app/config';
|
|||||||
import { parameterize } from '@/utility/string';
|
import { parameterize } from '@/utility/string';
|
||||||
import { PhotoSetCategory } from '../set';
|
import { PhotoSetCategory } from '../set';
|
||||||
import { Camera } from '@/camera';
|
import { Camera } from '@/camera';
|
||||||
|
import { Lens } from '@/lens';
|
||||||
|
|
||||||
export const GENERATE_STATIC_PARAMS_LIMIT = 1000;
|
export const GENERATE_STATIC_PARAMS_LIMIT = 1000;
|
||||||
export const PHOTO_DEFAULT_LIMIT = 100;
|
export const PHOTO_DEFAULT_LIMIT = 100;
|
||||||
@ -23,8 +24,9 @@ export type GetPhotosOptions = {
|
|||||||
takenAfterInclusive?: Date
|
takenAfterInclusive?: Date
|
||||||
updatedBefore?: Date
|
updatedBefore?: Date
|
||||||
hidden?: 'exclude' | 'include' | 'only'
|
hidden?: 'exclude' | 'include' | 'only'
|
||||||
} & Omit<PhotoSetCategory, 'camera'> & {
|
} & Omit<PhotoSetCategory, 'camera' | 'lens'> & {
|
||||||
camera?: Partial<Camera>
|
camera?: Partial<Camera>
|
||||||
|
lens?: Partial<Lens>
|
||||||
};
|
};
|
||||||
|
|
||||||
export const areOptionsSensitive = (options: GetPhotosOptions) =>
|
export const areOptionsSensitive = (options: GetPhotosOptions) =>
|
||||||
@ -83,10 +85,6 @@ export const getWheresFromOptions = (
|
|||||||
wheres.push(`aspect_ratio <= $${valuesIndex++}`);
|
wheres.push(`aspect_ratio <= $${valuesIndex++}`);
|
||||||
wheresValues.push(maximumAspectRatio);
|
wheresValues.push(maximumAspectRatio);
|
||||||
}
|
}
|
||||||
if (tag) {
|
|
||||||
wheres.push(`$${valuesIndex++}=ANY(tags)`);
|
|
||||||
wheresValues.push(tag);
|
|
||||||
}
|
|
||||||
if (camera?.make) {
|
if (camera?.make) {
|
||||||
wheres.push(`${parameterizeForDb('make')}=$${valuesIndex++}`);
|
wheres.push(`${parameterizeForDb('make')}=$${valuesIndex++}`);
|
||||||
wheresValues.push(parameterize(camera.make, true));
|
wheresValues.push(parameterize(camera.make, true));
|
||||||
@ -95,12 +93,18 @@ export const getWheresFromOptions = (
|
|||||||
wheres.push(`${parameterizeForDb('model')}=$${valuesIndex++}`);
|
wheres.push(`${parameterizeForDb('model')}=$${valuesIndex++}`);
|
||||||
wheresValues.push(parameterize(camera.model, true));
|
wheresValues.push(parameterize(camera.model, true));
|
||||||
}
|
}
|
||||||
if (lens) {
|
if (lens?.make) {
|
||||||
wheres.push(`${parameterizeForDb('lens_make')}=$${valuesIndex++}`);
|
wheres.push(`${parameterizeForDb('lens_make')}=$${valuesIndex++}`);
|
||||||
wheresValues.push(parameterize(lens.make, true));
|
wheresValues.push(parameterize(lens.make, true));
|
||||||
|
}
|
||||||
|
if (lens?.model) {
|
||||||
wheres.push(`${parameterizeForDb('lens_model')}=$${valuesIndex++}`);
|
wheres.push(`${parameterizeForDb('lens_model')}=$${valuesIndex++}`);
|
||||||
wheresValues.push(parameterize(lens.model, true));
|
wheresValues.push(parameterize(lens.model, true));
|
||||||
}
|
}
|
||||||
|
if (tag) {
|
||||||
|
wheres.push(`$${valuesIndex++}=ANY(tags)`);
|
||||||
|
wheresValues.push(tag);
|
||||||
|
}
|
||||||
if (simulation) {
|
if (simulation) {
|
||||||
wheres.push(`film_simulation=$${valuesIndex++}`);
|
wheres.push(`film_simulation=$${valuesIndex++}`);
|
||||||
wheresValues.push(simulation);
|
wheresValues.push(simulation);
|
||||||
|
|||||||
@ -334,6 +334,24 @@ export const getUniqueCameras = async () =>
|
|||||||
})))
|
})))
|
||||||
, 'getUniqueCameras');
|
, '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 () =>
|
export const getUniqueRecipes = async () =>
|
||||||
safelyQueryPhotos(() => sql`
|
safelyQueryPhotos(() => sql`
|
||||||
SELECT DISTINCT recipe_title, COUNT(*)
|
SELECT DISTINCT recipe_title, COUNT(*)
|
||||||
@ -405,24 +423,6 @@ export const getUniqueFilmSimulations = async () =>
|
|||||||
})))
|
})))
|
||||||
, 'getUniqueFilmSimulations');
|
, '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 () =>
|
export const getUniqueFocalLengths = async () =>
|
||||||
safelyQueryPhotos(() => sql`
|
safelyQueryPhotos(() => sql`
|
||||||
SELECT DISTINCT focal_length, COUNT(*)
|
SELECT DISTINCT focal_length, COUNT(*)
|
||||||
|
|||||||
@ -31,12 +31,12 @@ export const getHiddenDefaultCategories = (keys: CategoryKeys): CategoryKeys =>
|
|||||||
DEFAULT_CATEGORY_KEYS.filter(key => !keys.includes(key));
|
DEFAULT_CATEGORY_KEYS.filter(key => !keys.includes(key));
|
||||||
|
|
||||||
export interface PhotoSetCategory {
|
export interface PhotoSetCategory {
|
||||||
tag?: string
|
|
||||||
camera?: Camera
|
camera?: Camera
|
||||||
|
lens?: Lens
|
||||||
|
tag?: string
|
||||||
recipe?: string
|
recipe?: string
|
||||||
simulation?: FilmSimulation
|
simulation?: FilmSimulation
|
||||||
focal?: number
|
focal?: number
|
||||||
lens?: Lens // Unimplemented as a set
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PhotoSetCategories {
|
export interface PhotoSetCategories {
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import FilmSimulationShareModal from '@/simulation/FilmSimulationShareModal';
|
|||||||
import FocalLengthShareModal from '@/focal/FocalLengthShareModal';
|
import FocalLengthShareModal from '@/focal/FocalLengthShareModal';
|
||||||
import { useAppState } from '@/state/AppState';
|
import { useAppState } from '@/state/AppState';
|
||||||
import RecipeShareModal from '@/recipe/RecipeShareModal';
|
import RecipeShareModal from '@/recipe/RecipeShareModal';
|
||||||
|
import LensShareModal from '@/lens/LensShareModal';
|
||||||
|
|
||||||
export default function ShareModals() {
|
export default function ShareModals() {
|
||||||
const { shareModalProps = {} } = useAppState();
|
const { shareModalProps = {} } = useAppState();
|
||||||
@ -16,8 +17,9 @@ export default function ShareModals() {
|
|||||||
photos,
|
photos,
|
||||||
count,
|
count,
|
||||||
dateRange,
|
dateRange,
|
||||||
tag,
|
|
||||||
camera,
|
camera,
|
||||||
|
lens,
|
||||||
|
tag,
|
||||||
simulation,
|
simulation,
|
||||||
recipe,
|
recipe,
|
||||||
focal,
|
focal,
|
||||||
@ -38,6 +40,8 @@ export default function ShareModals() {
|
|||||||
return <TagShareModal {...{ tag, ...attributes }} />;
|
return <TagShareModal {...{ tag, ...attributes }} />;
|
||||||
} else if (camera) {
|
} else if (camera) {
|
||||||
return <CameraShareModal {...{ camera, ...attributes }} />;
|
return <CameraShareModal {...{ camera, ...attributes }} />;
|
||||||
|
} else if (lens) {
|
||||||
|
return <LensShareModal {...{ lens, ...attributes }} />;
|
||||||
} else if (simulation) {
|
} else if (simulation) {
|
||||||
return <FilmSimulationShareModal {...{ simulation, ...attributes }} />;
|
return <FilmSimulationShareModal {...{ simulation, ...attributes }} />;
|
||||||
} else if (recipe) {
|
} else if (recipe) {
|
||||||
|
|||||||
@ -4,7 +4,9 @@ import {
|
|||||||
absolutePathForCameraImage,
|
absolutePathForCameraImage,
|
||||||
absolutePathForFilmSimulationImage,
|
absolutePathForFilmSimulationImage,
|
||||||
absolutePathForFocalLengthImage,
|
absolutePathForFocalLengthImage,
|
||||||
|
absolutePathForLensImage,
|
||||||
absolutePathForPhotoImage,
|
absolutePathForPhotoImage,
|
||||||
|
absolutePathForRecipeImage,
|
||||||
absolutePathForTagImage,
|
absolutePathForTagImage,
|
||||||
} from '@/app/paths';
|
} from '@/app/paths';
|
||||||
|
|
||||||
@ -15,24 +17,26 @@ export type ShareModalProps = Omit<PhotoSetAttributes, 'photos'> & {
|
|||||||
|
|
||||||
export const getSharePathFromShareModalProps = ({
|
export const getSharePathFromShareModalProps = ({
|
||||||
photo,
|
photo,
|
||||||
tag,
|
|
||||||
camera,
|
camera,
|
||||||
|
lens,
|
||||||
|
tag,
|
||||||
|
recipe,
|
||||||
simulation,
|
simulation,
|
||||||
focal,
|
focal,
|
||||||
}: ShareModalProps) => {
|
}: ShareModalProps) => {
|
||||||
if (photo) {
|
if (photo) {
|
||||||
return absolutePathForPhotoImage(photo);
|
return absolutePathForPhotoImage(photo);
|
||||||
}
|
} else if (camera) {
|
||||||
if (tag) {
|
|
||||||
return absolutePathForTagImage(tag);
|
|
||||||
}
|
|
||||||
if (camera) {
|
|
||||||
return absolutePathForCameraImage(camera);
|
return absolutePathForCameraImage(camera);
|
||||||
}
|
} else if (lens) {
|
||||||
if (simulation) {
|
return absolutePathForLensImage(lens);
|
||||||
|
} else if (tag) {
|
||||||
|
return absolutePathForTagImage(tag);
|
||||||
|
} else if (recipe) {
|
||||||
|
return absolutePathForRecipeImage(recipe);
|
||||||
|
} else if (simulation) {
|
||||||
return absolutePathForFilmSimulationImage(simulation);
|
return absolutePathForFilmSimulationImage(simulation);
|
||||||
}
|
} else if (focal) {
|
||||||
if (focal) {
|
|
||||||
return absolutePathForFocalLengthImage(focal);
|
return absolutePathForFocalLengthImage(focal);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user