Refactor og image generation
This commit is contained in:
parent
0af79f491a
commit
9bdf0c3f09
@ -1,29 +1,27 @@
|
||||
import { auth } from '@/auth';
|
||||
import { getImageCacheHeadersForAuth } from '@/cache';
|
||||
import {
|
||||
IMAGE_OG_SMALL_SIZE,
|
||||
MAX_PHOTOS_TO_SHOW_HOME,
|
||||
} from '@/photo/image-response';
|
||||
import HomeImageResponse from '@/photo/image-response/HomeImageResponse';
|
||||
import { getPhotos } from '@/services/postgres';
|
||||
import { IMAGE_OG_SMALL_HEIGHT, IMAGE_OG_SMALL_WIDTH } from '@/site';
|
||||
import { ImageResponse } from '@vercel/og';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const photos = await getPhotos();
|
||||
const photos = await getPhotos(
|
||||
undefined,
|
||||
MAX_PHOTOS_TO_SHOW_HOME,
|
||||
);
|
||||
|
||||
const headers = await getImageCacheHeadersForAuth(await auth());
|
||||
|
||||
const { width, height } = IMAGE_OG_SMALL_SIZE;
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<HomeImageResponse {...{
|
||||
photos,
|
||||
request,
|
||||
width: IMAGE_OG_SMALL_WIDTH,
|
||||
height: IMAGE_OG_SMALL_HEIGHT,
|
||||
}}/>
|
||||
),
|
||||
{
|
||||
width: IMAGE_OG_SMALL_WIDTH,
|
||||
height: IMAGE_OG_SMALL_HEIGHT,
|
||||
headers,
|
||||
},
|
||||
<HomeImageResponse {...{ photos, request, width, height }}/>,
|
||||
{ width, height, headers },
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,42 +1,29 @@
|
||||
import { auth } from '@/auth';
|
||||
import { getImageCacheHeadersForAuth } from '@/cache';
|
||||
import PhotoOGImageResponse from '@/photo/image-response/PhotoOGImageResponse';
|
||||
import { IMAGE_OG_SIZE } from '@/photo/image-response';
|
||||
import PhotoImageResponse from '@/photo/image-response/PhotoImageResponse';
|
||||
import { getPhoto } from '@/services/postgres';
|
||||
import { IMAGE_OG_WIDTH, IMAGE_OG_HEIGHT } from '@/site';
|
||||
import { FONT_FAMILY_IBM_PLEX_MONO, getIBMPlexMonoMedium } from '@/site/font';
|
||||
import { getIBMPlexMonoMedium } from '@/site/font';
|
||||
import { ImageResponse } from '@vercel/og';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
export async function GET(request: Request, context: any) {
|
||||
const photo = await getPhoto(context.params.photoId);
|
||||
const fontData = await getIBMPlexMonoMedium();
|
||||
|
||||
const {
|
||||
fontFamily,
|
||||
fonts,
|
||||
} = await getIBMPlexMonoMedium();
|
||||
|
||||
const headers = await getImageCacheHeadersForAuth(await auth());
|
||||
|
||||
if (!photo) { return null; }
|
||||
|
||||
const { width, height } = IMAGE_OG_SIZE;
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<PhotoOGImageResponse
|
||||
photo={photo}
|
||||
requestOrPhotoPath={request}
|
||||
width={IMAGE_OG_WIDTH}
|
||||
height={IMAGE_OG_HEIGHT}
|
||||
fontFamily={FONT_FAMILY_IBM_PLEX_MONO}
|
||||
/>
|
||||
),
|
||||
{
|
||||
width: IMAGE_OG_WIDTH,
|
||||
height: IMAGE_OG_HEIGHT,
|
||||
fonts: [
|
||||
{
|
||||
name: FONT_FAMILY_IBM_PLEX_MONO,
|
||||
data: fontData,
|
||||
weight: 500,
|
||||
style: 'normal',
|
||||
},
|
||||
],
|
||||
headers,
|
||||
},
|
||||
<PhotoImageResponse {...{ photo, request, width, height, fontFamily }} />,
|
||||
{ width, height, fonts, headers },
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,30 +1,35 @@
|
||||
import { auth } from '@/auth';
|
||||
import { getImageCacheHeadersForAuth } from '@/cache';
|
||||
import HomeImageResponse from '@/photo/image-response/HomeImageResponse';
|
||||
import {
|
||||
IMAGE_OG_SMALL_SIZE,
|
||||
MAX_PHOTOS_TO_SHOW_PER_TAG,
|
||||
} from '@/photo/image-response';
|
||||
import TagImageResponse from '@/photo/image-response/TagImageResponse';
|
||||
import { getPhotos } from '@/services/postgres';
|
||||
import { IMAGE_OG_SMALL_HEIGHT, IMAGE_OG_SMALL_WIDTH } from '@/site';
|
||||
import { getIBMPlexMonoMedium } from '@/site/font';
|
||||
import { ImageResponse } from '@vercel/og';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
export async function GET(request: Request, context: any) {
|
||||
const photos = await getPhotos(
|
||||
undefined, undefined, undefined, context.params.tag);
|
||||
undefined,
|
||||
MAX_PHOTOS_TO_SHOW_PER_TAG,
|
||||
undefined,
|
||||
context.params.tag,
|
||||
);
|
||||
|
||||
const {
|
||||
fontFamily,
|
||||
fonts,
|
||||
} = await getIBMPlexMonoMedium();
|
||||
|
||||
const headers = await getImageCacheHeadersForAuth(await auth());
|
||||
|
||||
const { width, height } = IMAGE_OG_SMALL_SIZE;
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<HomeImageResponse {...{
|
||||
photos,
|
||||
request,
|
||||
width: IMAGE_OG_SMALL_WIDTH,
|
||||
height: IMAGE_OG_SMALL_HEIGHT,
|
||||
}}/>
|
||||
),
|
||||
{
|
||||
width: IMAGE_OG_SMALL_WIDTH,
|
||||
height: IMAGE_OG_SMALL_HEIGHT,
|
||||
headers,
|
||||
},
|
||||
<TagImageResponse {...{ photos, request, width, height, fontFamily }}/>,
|
||||
{ width, height, fonts, headers },
|
||||
);
|
||||
}
|
||||
|
||||
@ -53,13 +53,13 @@ export default async function TagPage({ params: { tag } }: TagProps) {
|
||||
<SiteGrid
|
||||
contentMain={<div className="space-y-8 mt-4">
|
||||
<div className={cc(
|
||||
'flex gap-2 sm:gap-0',
|
||||
'sm:grid sm:grid-cols-4 md:grid-cols-3 lg:grid-cols-4',
|
||||
'flex flex-col gap-y-0.5',
|
||||
'xs:grid grid-cols-2 sm:grid-cols-4 md:grid-cols-3 lg:grid-cols-4',
|
||||
)}>
|
||||
<PhotoTag tag={tag} />
|
||||
<span className={cc(
|
||||
'uppercase text-gray-300 dark:text-gray-500',
|
||||
'col-span-2 md:col-span-1 lg:col-span-2',
|
||||
'sm:col-span-2 md:col-span-1 lg:col-span-2',
|
||||
)}>
|
||||
{descriptionForTaggedPhotos(photos)}
|
||||
</span>
|
||||
|
||||
@ -1,41 +1,45 @@
|
||||
import { auth } from '@/auth';
|
||||
import { getImageCacheHeadersForAuth } from '@/cache';
|
||||
import DeployImageResponse from '@/photo/image-response/DeployImageResponse';
|
||||
import {
|
||||
IMAGE_OG_SIZE,
|
||||
MAX_PHOTOS_TO_SHOW_TEMPLATE_TIGHT,
|
||||
} from '@/photo/image-response';
|
||||
import TemplateImageResponse from
|
||||
'@/photo/image-response/TemplateImageResponse';
|
||||
import { getPhotos } from '@/services/postgres';
|
||||
import { IMAGE_OG_HEIGHT, IMAGE_OG_WIDTH } from '@/site';
|
||||
import { FONT_FAMILY_IBM_PLEX_MONO, getIBMPlexMonoMedium } from '@/site/font';
|
||||
import { getIBMPlexMonoMedium } from '@/site/font';
|
||||
import { ImageResponse } from '@vercel/og';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const photos = await getPhotos('priority');
|
||||
const fontData = await getIBMPlexMonoMedium();
|
||||
const photos = await getPhotos('priority', MAX_PHOTOS_TO_SHOW_TEMPLATE_TIGHT);
|
||||
|
||||
const {
|
||||
fontFamily,
|
||||
fonts,
|
||||
} = await getIBMPlexMonoMedium();
|
||||
|
||||
const headers = await getImageCacheHeadersForAuth(await auth());
|
||||
|
||||
const { width, height } = IMAGE_OG_SIZE;
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<DeployImageResponse {...{
|
||||
<TemplateImageResponse {...{
|
||||
photos,
|
||||
request,
|
||||
includeHeader: false,
|
||||
outerMargin: 0,
|
||||
width: IMAGE_OG_WIDTH,
|
||||
height: IMAGE_OG_HEIGHT,
|
||||
verticalOffset: -30,
|
||||
fontFamily: FONT_FAMILY_IBM_PLEX_MONO,
|
||||
width,
|
||||
height,
|
||||
fontFamily,
|
||||
}}/>
|
||||
),
|
||||
{
|
||||
width: IMAGE_OG_WIDTH,
|
||||
height: IMAGE_OG_HEIGHT,
|
||||
fonts: [
|
||||
{
|
||||
name: FONT_FAMILY_IBM_PLEX_MONO,
|
||||
data: fontData,
|
||||
style: 'normal',
|
||||
},
|
||||
],
|
||||
width,
|
||||
height,
|
||||
fonts,
|
||||
headers,
|
||||
},
|
||||
);
|
||||
|
||||
@ -1,39 +1,37 @@
|
||||
import { auth } from '@/auth';
|
||||
import { getImageCacheHeadersForAuth } from '@/cache';
|
||||
import DeployImageResponse from '@/photo/image-response/DeployImageResponse';
|
||||
import {
|
||||
GRID_OG_SIZE,
|
||||
MAX_PHOTOS_TO_SHOW_TEMPLATE,
|
||||
} from '@/photo/image-response';
|
||||
import TemplateImageResponse from
|
||||
'@/photo/image-response/TemplateImageResponse';
|
||||
import { getPhotos } from '@/services/postgres';
|
||||
import { GRID_OG_WIDTH, GRID_OG_HEIGHT } from '@/site';
|
||||
import { FONT_FAMILY_IBM_PLEX_MONO, getIBMPlexMonoMedium } from '@/site/font';
|
||||
import { getIBMPlexMonoMedium } from '@/site/font';
|
||||
import { ImageResponse } from '@vercel/og';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const photos = await getPhotos('priority');
|
||||
const fontData = await getIBMPlexMonoMedium();
|
||||
const photos = await getPhotos('priority', MAX_PHOTOS_TO_SHOW_TEMPLATE);
|
||||
const {
|
||||
fontFamily,
|
||||
fonts,
|
||||
} = await getIBMPlexMonoMedium();
|
||||
const headers = await getImageCacheHeadersForAuth(await auth());
|
||||
|
||||
const { width, height } = GRID_OG_SIZE;
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<DeployImageResponse {...{
|
||||
<TemplateImageResponse {...{
|
||||
photos,
|
||||
request,
|
||||
width: GRID_OG_WIDTH,
|
||||
height: GRID_OG_HEIGHT,
|
||||
fontFamily: FONT_FAMILY_IBM_PLEX_MONO,
|
||||
width,
|
||||
height,
|
||||
fontFamily,
|
||||
}}/>
|
||||
),
|
||||
{
|
||||
width: GRID_OG_WIDTH,
|
||||
height: GRID_OG_HEIGHT,
|
||||
fonts: [
|
||||
{
|
||||
name: FONT_FAMILY_IBM_PLEX_MONO,
|
||||
data: fontData,
|
||||
style: 'normal',
|
||||
},
|
||||
],
|
||||
headers,
|
||||
},
|
||||
{ width, height, fonts, headers },
|
||||
);
|
||||
}
|
||||
|
||||
@ -9,9 +9,9 @@ import {
|
||||
import { cc } from '@/utility/css';
|
||||
import Link from 'next/link';
|
||||
import { BiError } from 'react-icons/bi';
|
||||
import { IMAGE_OG_HEIGHT, IMAGE_OG_RATIO, IMAGE_OG_WIDTH } from '@/site';
|
||||
import { absolutePathForPhotoImage, pathForPhoto } from '@/site/paths';
|
||||
import Spinner from '@/components/Spinner';
|
||||
import { IMAGE_OG_SIZE } from './image-response';
|
||||
|
||||
export type OGLoadingState = 'unloaded' | 'loading' | 'loaded' | 'failed';
|
||||
|
||||
@ -44,6 +44,8 @@ export default function PhotoOGTile({
|
||||
}
|
||||
}, [loadingStateExternal, loadingStateInternal]);
|
||||
|
||||
const { width, height, ratio } = IMAGE_OG_SIZE;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={photo.id}
|
||||
@ -58,7 +60,7 @@ export default function PhotoOGTile({
|
||||
>
|
||||
<div
|
||||
className="relative"
|
||||
style={{ aspectRatio: IMAGE_OG_RATIO }}
|
||||
style={{ aspectRatio: ratio }}
|
||||
>
|
||||
{loadingState === 'loading' &&
|
||||
<div className={cc(
|
||||
@ -85,8 +87,8 @@ export default function PhotoOGTile({
|
||||
'transition-opacity',
|
||||
)}
|
||||
src={absolutePathForPhotoImage(photo)}
|
||||
width={IMAGE_OG_WIDTH}
|
||||
height={IMAGE_OG_HEIGHT}
|
||||
width={width}
|
||||
height={height}
|
||||
onLoad={() => {
|
||||
if (onLoad) {
|
||||
onLoad();
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Photo } from '..';
|
||||
import PhotoGridImageResponse from './PhotoGridImageResponse';
|
||||
import ImageContainer from './components/ImageContainer';
|
||||
import ImagePhotoGrid from './components/ImagePhotoGrid';
|
||||
|
||||
export default function HomeImageResponse({
|
||||
photos,
|
||||
@ -12,31 +13,16 @@ export default function HomeImageResponse({
|
||||
width: number
|
||||
height: number
|
||||
}) {
|
||||
const grid = photos.length >= 12
|
||||
? { colCount: 4, rowCount: 3 }
|
||||
: { colCount: 3, rowCount: 2 };
|
||||
|
||||
const photosPerGrid = grid.colCount * grid.rowCount;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
background: 'transparent',
|
||||
width,
|
||||
height,
|
||||
}}>
|
||||
<PhotoGridImageResponse {...{
|
||||
photos: photos.slice(0, photosPerGrid),
|
||||
request,
|
||||
nextImageWidth: 200,
|
||||
...grid,
|
||||
gap: 6,
|
||||
width,
|
||||
height,
|
||||
}} />
|
||||
</div>
|
||||
<ImageContainer {...{ width, height }} >
|
||||
<ImagePhotoGrid
|
||||
{...{
|
||||
photos,
|
||||
request,
|
||||
width,
|
||||
height,
|
||||
}}
|
||||
/>
|
||||
</ImageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,87 +0,0 @@
|
||||
import { getNextImageUrlForRequest } from '@/utility/image';
|
||||
import { Photo, titleForPhoto } from '..';
|
||||
|
||||
export default function PhotoGridImageResponse({
|
||||
photos,
|
||||
request,
|
||||
nextImageWidth = 400,
|
||||
height,
|
||||
width,
|
||||
colCount,
|
||||
rowCount,
|
||||
gap = 12,
|
||||
verticalOffset,
|
||||
}: {
|
||||
photos: Photo[]
|
||||
request: Request
|
||||
nextImageWidth?: 200 | 400,
|
||||
height?: number
|
||||
width: number
|
||||
colCount: number
|
||||
rowCount: number
|
||||
gap?: number
|
||||
verticalOffset?: number
|
||||
}) {
|
||||
const imageWidth = (width - ((colCount - 1) * gap)) / colCount;
|
||||
const imageHeight = height
|
||||
? (height - ((rowCount - 1) * gap)) / rowCount
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
...verticalOffset && { transform: `translateY(${verticalOffset}px)` },
|
||||
}}>
|
||||
{photos
|
||||
.slice(0, colCount * rowCount)
|
||||
.map((photo, index) => {
|
||||
const photoWidth = imageHeight
|
||||
? imageHeight * photo.aspectRatio
|
||||
: imageWidth;
|
||||
const photoHeight = imageHeight ?? imageWidth / photo.aspectRatio;
|
||||
const horizontalOffset = imageHeight
|
||||
? Math.abs((imageHeight * photo.aspectRatio - imageWidth) / 2)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={photo.id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
position: 'relative',
|
||||
width: imageWidth,
|
||||
height: imageHeight ?? imageWidth / photo.aspectRatio,
|
||||
...(index + 1) % colCount !== 0 && {
|
||||
marginRight: gap,
|
||||
},
|
||||
...index < colCount * (rowCount - 1) && {
|
||||
marginBottom: gap,
|
||||
},
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={getNextImageUrlForRequest(
|
||||
photo.url,
|
||||
request,
|
||||
nextImageWidth,
|
||||
)}
|
||||
alt={titleForPhoto(photo)}
|
||||
width={nextImageWidth}
|
||||
height={nextImageWidth / photo.aspectRatio}
|
||||
style={{
|
||||
display: 'flex',
|
||||
width: photoWidth,
|
||||
height: photoHeight,
|
||||
...horizontalOffset && {
|
||||
marginLeft: `-${horizontalOffset}px`,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
55
src/photo/image-response/PhotoImageResponse.tsx
Normal file
55
src/photo/image-response/PhotoImageResponse.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import { Photo } from '..';
|
||||
import { NextImageWidth } from '@/utility/image';
|
||||
import { formatModelShort } from '@/utility/exif';
|
||||
import { AiFillApple } from 'react-icons/ai';
|
||||
import ImageCaption from './components/ImageCaption';
|
||||
import ImagePhotoGrid from './components/ImagePhotoGrid';
|
||||
|
||||
export default function PhotoImageResponse({
|
||||
photo,
|
||||
request,
|
||||
width,
|
||||
height,
|
||||
fontFamily,
|
||||
}: {
|
||||
photo: Photo
|
||||
request: Request
|
||||
width: NextImageWidth
|
||||
height: number
|
||||
fontFamily: string
|
||||
}) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
position: 'relative',
|
||||
background: 'red',
|
||||
width,
|
||||
height,
|
||||
}}>
|
||||
<ImagePhotoGrid {...{
|
||||
photos: [photo],
|
||||
request,
|
||||
width,
|
||||
height,
|
||||
}} />
|
||||
<ImageCaption {...{ width, height, fontFamily }}>
|
||||
{photo.make === 'Apple' &&
|
||||
<div style={{ display: 'flex' }}>
|
||||
<AiFillApple />
|
||||
</div>}
|
||||
<div style={{ display: 'flex' }}>
|
||||
{formatModelShort(photo.model)}
|
||||
</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
{photo.focalLengthFormatted}
|
||||
</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
{photo.fNumberFormatted}
|
||||
</div>
|
||||
<div>
|
||||
{photo.isoFormatted}
|
||||
</div>
|
||||
</ImageCaption>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,75 +0,0 @@
|
||||
import { Photo, titleForPhoto } from '..';
|
||||
import { getNextImageUrlForRequest } from '@/utility/image';
|
||||
import { formatModelShort } from '@/utility/exif';
|
||||
import { AiFillApple } from 'react-icons/ai';
|
||||
|
||||
export default function PhotoOGImageResponse({
|
||||
photo,
|
||||
requestOrPhotoPath,
|
||||
width,
|
||||
height,
|
||||
fontFamily,
|
||||
}: {
|
||||
photo: Photo
|
||||
requestOrPhotoPath: Request | string
|
||||
width: number
|
||||
height: number
|
||||
fontFamily: string
|
||||
}) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
position: 'relative',
|
||||
background: 'red',
|
||||
width,
|
||||
height,
|
||||
}}>
|
||||
<img
|
||||
src={typeof requestOrPhotoPath === 'string'
|
||||
? requestOrPhotoPath
|
||||
: getNextImageUrlForRequest(
|
||||
photo.url,
|
||||
requestOrPhotoPath,
|
||||
width,
|
||||
)}
|
||||
width={width}
|
||||
height={width / photo.aspectRatio}
|
||||
alt={titleForPhoto(photo)}
|
||||
/>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: 36,
|
||||
position: 'absolute',
|
||||
padding: '400px 56px 48px 56px',
|
||||
color: 'white',
|
||||
background:
|
||||
'linear-gradient(to bottom, ' +
|
||||
'rgba(0,0,0,0), rgba(0,0,0,0.3), rgba(0,0,0,0.7))',
|
||||
backgroundBlendMode: 'multiply',
|
||||
fontFamily,
|
||||
fontSize: 60,
|
||||
lineHeight: 1,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
}}>
|
||||
{photo.make === 'Apple' &&
|
||||
<div style={{ display: 'flex' }}>
|
||||
<AiFillApple />
|
||||
</div>}
|
||||
<div style={{ display: 'flex' }}>
|
||||
{formatModelShort(photo.model)}
|
||||
</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
{photo.focalLengthFormatted}
|
||||
</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
{photo.fNumberFormatted}
|
||||
</div>
|
||||
<div>
|
||||
{photo.isoFormatted}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
43
src/photo/image-response/TagImageResponse.tsx
Normal file
43
src/photo/image-response/TagImageResponse.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { Photo } from '..';
|
||||
import { FaTag } from 'react-icons/fa';
|
||||
import ImageCaption from './components/ImageCaption';
|
||||
import ImagePhotoGrid from './components/ImagePhotoGrid';
|
||||
import ImageContainer from './components/ImageContainer';
|
||||
|
||||
export default function TagImageResponse({
|
||||
photos,
|
||||
request,
|
||||
width,
|
||||
height,
|
||||
fontFamily,
|
||||
}: {
|
||||
photos: Photo[]
|
||||
request: Request
|
||||
width: number
|
||||
height: number
|
||||
fontFamily: string
|
||||
}) {
|
||||
return (
|
||||
<ImageContainer {...{
|
||||
width,
|
||||
height,
|
||||
...photos.length === 0 && { background: 'black' },
|
||||
}}>
|
||||
<ImagePhotoGrid
|
||||
{...{
|
||||
photos,
|
||||
request,
|
||||
width,
|
||||
height,
|
||||
}}
|
||||
/>
|
||||
<ImageCaption {...{ width, height, fontFamily }}>
|
||||
<FaTag
|
||||
size={height * .067}
|
||||
style={{ transform: `translateY(${height * .02}px)` }}
|
||||
/>
|
||||
<span>{'Door County'.toUpperCase()}</span>
|
||||
</ImageCaption>
|
||||
</ImageContainer>
|
||||
);
|
||||
}
|
||||
@ -1,9 +1,9 @@
|
||||
import { Photo } from '..';
|
||||
import PhotoGridImageResponse from './PhotoGridImageResponse';
|
||||
import IconFullFrame from '@/icons/IconFullFrame';
|
||||
import IconGrid from '@/icons/IconGrid';
|
||||
import ImagePhotoGrid from './components/ImagePhotoGrid';
|
||||
|
||||
export default function DeployImageResponse({
|
||||
export default function TemplateImageResponse({
|
||||
photos,
|
||||
request,
|
||||
width,
|
||||
@ -40,16 +40,16 @@ export default function DeployImageResponse({
|
||||
height,
|
||||
fontFamily,
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontSize: 40,
|
||||
height: 80,
|
||||
lineHeight: 1,
|
||||
marginBottom: outerMargin,
|
||||
width: '100%',
|
||||
}}>
|
||||
{includeHeader &&
|
||||
{includeHeader &&
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontSize: 40,
|
||||
height: 80,
|
||||
lineHeight: 1,
|
||||
marginBottom: outerMargin,
|
||||
width: '100%',
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
@ -76,23 +76,29 @@ export default function DeployImageResponse({
|
||||
<IconGrid includeTitle={false} width={80} />
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
flexGrow: 1,
|
||||
}}>
|
||||
photos.sambecker.com
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
flexGrow: 1,
|
||||
}}>
|
||||
photos.sambecker.com
|
||||
</div>
|
||||
</div>}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
...verticalOffset && { transform: `translateY(${verticalOffset}px)` },
|
||||
}}>
|
||||
<ImagePhotoGrid {...{
|
||||
photos,
|
||||
request,
|
||||
width: innerWidth,
|
||||
height: includeHeader
|
||||
? height - 130 - outerMargin * 2
|
||||
: height,
|
||||
gap: 10,
|
||||
}} />
|
||||
</div>
|
||||
<PhotoGridImageResponse {...{
|
||||
photos,
|
||||
request,
|
||||
colCount: 4,
|
||||
rowCount: 4,
|
||||
width: innerWidth,
|
||||
verticalOffset,
|
||||
}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
35
src/photo/image-response/components/ImageCaption.tsx
Normal file
35
src/photo/image-response/components/ImageCaption.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
export default function ImageCaption({
|
||||
height,
|
||||
children,
|
||||
fontFamily,
|
||||
}: {
|
||||
width: number
|
||||
height: number
|
||||
fontFamily: string
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: height * .053,
|
||||
position: 'absolute',
|
||||
paddingTop: height * .6,
|
||||
paddingBottom: height * .075,
|
||||
paddingLeft: height * .0875,
|
||||
paddingRight: height * .0875,
|
||||
color: 'white',
|
||||
background:
|
||||
'linear-gradient(to bottom, ' +
|
||||
'rgba(0,0,0,0), rgba(0,0,0,0.3), rgba(0,0,0,0.7))',
|
||||
backgroundBlendMode: 'multiply',
|
||||
fontFamily,
|
||||
fontSize: height *.089,
|
||||
lineHeight: 1,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
27
src/photo/image-response/components/ImageContainer.tsx
Normal file
27
src/photo/image-response/components/ImageContainer.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export default function ImageContainer({
|
||||
width,
|
||||
height,
|
||||
background = 'transparent',
|
||||
children,
|
||||
}: {
|
||||
width: number
|
||||
height: number
|
||||
background?: 'transparent' | 'black'
|
||||
children: ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div style={{
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background,
|
||||
width,
|
||||
height,
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
64
src/photo/image-response/components/ImagePhotoGrid.tsx
Normal file
64
src/photo/image-response/components/ImagePhotoGrid.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
/* eslint-disable jsx-a11y/alt-text */
|
||||
|
||||
import { Photo } from '@/photo';
|
||||
import { getNextImageUrlForRequest } from '@/utility/image';
|
||||
|
||||
export default function ImagePhotoGrid({
|
||||
photos,
|
||||
request,
|
||||
width,
|
||||
height,
|
||||
gap = 3,
|
||||
}: {
|
||||
photos: Photo[]
|
||||
request: Request
|
||||
width: number
|
||||
height: number
|
||||
gap?: number
|
||||
}) {
|
||||
let count = 1;
|
||||
if (photos.length >= 12) { count = 12; }
|
||||
else if (photos.length >= 6) { count = 6; }
|
||||
else if (photos.length >= 4) { count = 4; }
|
||||
else if (photos.length >= 2) { count = 2; }
|
||||
|
||||
const imageQuality = count <= 2 ? 1050 : 400;
|
||||
|
||||
let rows = 1;
|
||||
if (count > 12) { rows = 4; }
|
||||
else if (count > 6) { rows = 3; }
|
||||
else if (count > 3) { rows = 2; }
|
||||
|
||||
const imagesPerRow = count / rows;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap,
|
||||
}}>
|
||||
{photos.slice(0, count).map(photo =>
|
||||
<div
|
||||
key={photo.id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
width:
|
||||
width / imagesPerRow -
|
||||
(imagesPerRow - 1) * gap / (imagesPerRow),
|
||||
height: height / rows - (rows - 1) * gap / rows,
|
||||
}}
|
||||
>
|
||||
<img {...{
|
||||
src: getNextImageUrlForRequest(photo.url, request, imageQuality),
|
||||
style: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
},
|
||||
}} />
|
||||
</div>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
35
src/photo/image-response/index.ts
Normal file
35
src/photo/image-response/index.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { NextImageWidth } from '@/utility/image';
|
||||
|
||||
export const MAX_PHOTOS_TO_SHOW_HOME = 12;
|
||||
export const MAX_PHOTOS_TO_SHOW_PER_TAG = 6;
|
||||
export const MAX_PHOTOS_TO_SHOW_TEMPLATE = 16;
|
||||
export const MAX_PHOTOS_TO_SHOW_TEMPLATE_TIGHT = 12;
|
||||
|
||||
// 16:9 og image ratio
|
||||
const IMAGE_OG_RATIO = 16 / 9;
|
||||
const IMAGE_OG_WIDTH: NextImageWidth = 1200;
|
||||
const IMAGE_OG_HEIGHT = IMAGE_OG_WIDTH * (1 / IMAGE_OG_RATIO);
|
||||
export const IMAGE_OG_SIZE = {
|
||||
width: IMAGE_OG_WIDTH,
|
||||
height: IMAGE_OG_HEIGHT,
|
||||
ratio: IMAGE_OG_RATIO,
|
||||
};
|
||||
|
||||
// 16:9 og image ratio, small
|
||||
const IMAGE_OG_SMALL_WIDTH = 800;
|
||||
const IMAGE_OG_SMALL_HEIGHT = IMAGE_OG_SMALL_WIDTH * (1 / IMAGE_OG_RATIO);
|
||||
export const IMAGE_OG_SMALL_SIZE = {
|
||||
width: IMAGE_OG_SMALL_WIDTH,
|
||||
height: IMAGE_OG_SMALL_HEIGHT,
|
||||
ratio: IMAGE_OG_RATIO,
|
||||
};
|
||||
|
||||
// 3:2 og grid ratio
|
||||
const GRID_OG_RATIO = 1.33;
|
||||
const GRID_OG_WIDTH = 2000;
|
||||
const GRID_OG_HEIGHT = GRID_OG_WIDTH * (1 / GRID_OG_RATIO);
|
||||
export const GRID_OG_SIZE = {
|
||||
width: GRID_OG_WIDTH,
|
||||
height: GRID_OG_HEIGHT,
|
||||
ratio: GRID_OG_RATIO,
|
||||
};
|
||||
@ -1,8 +1,14 @@
|
||||
import { BLOB_BASE_URL } from '@/site';
|
||||
import { PATH_ADMIN_UPLOAD_BLOB_HANDLER } from '@/site/paths';
|
||||
import { del, list, put } from '@vercel/blob';
|
||||
import { upload } from '@vercel/blob/client';
|
||||
|
||||
const STORE_ID = process.env.BLOB_READ_WRITE_TOKEN?.match(
|
||||
/^vercel_blob_rw_([a-z0-9]+)_[a-z0-9]+$/i,
|
||||
)?.[1].toLowerCase();
|
||||
|
||||
export const BLOB_BASE_URL =
|
||||
`https://${STORE_ID}.public.blob.vercel-storage.com`;
|
||||
|
||||
export const ACCEPTED_PHOTO_FILE_TYPES = [
|
||||
'image/jpg',
|
||||
'image/jpeg',
|
||||
|
||||
@ -1,7 +1,16 @@
|
||||
export const FONT_FAMILY_IBM_PLEX_MONO = 'IBMPlexMono';
|
||||
const FONT_FAMILY_IBM_PLEX_MONO = 'IBMPlexMono';
|
||||
|
||||
export const getIBMPlexMonoMedium = () => fetch(new URL(
|
||||
'/public/fonts/IBMPlexMono-Medium.ttf',
|
||||
import.meta.url
|
||||
))
|
||||
.then(res => res.arrayBuffer());
|
||||
.then(res => res.arrayBuffer())
|
||||
.then(data => ({
|
||||
fontFamily: FONT_FAMILY_IBM_PLEX_MONO,
|
||||
fonts: [{
|
||||
name: FONT_FAMILY_IBM_PLEX_MONO,
|
||||
data,
|
||||
weight: 500,
|
||||
style: 'normal',
|
||||
} as const],
|
||||
}));
|
||||
|
||||
@ -6,26 +6,3 @@ export const IMAGE_SMALL_WIDTH = 300;
|
||||
|
||||
// Height determined by intrinsic photo aspect ratio
|
||||
export const IMAGE_LARGE_WIDTH = 900;
|
||||
|
||||
// 16:9 og image ratio
|
||||
export const IMAGE_OG_RATIO = 16 / 9;
|
||||
export const IMAGE_OG_WIDTH = 1200;
|
||||
export const IMAGE_OG_HEIGHT =
|
||||
IMAGE_OG_WIDTH * (1 / IMAGE_OG_RATIO);
|
||||
|
||||
// 16:9 og image ratio, small
|
||||
export const IMAGE_OG_SMALL_WIDTH = 800;
|
||||
export const IMAGE_OG_SMALL_HEIGHT =
|
||||
IMAGE_OG_SMALL_WIDTH * (1 / IMAGE_OG_RATIO);
|
||||
|
||||
// 3:2 og grid ratio
|
||||
export const GRID_OG_RATIO = 1.33;
|
||||
export const GRID_OG_WIDTH = 2000;
|
||||
export const GRID_OG_HEIGHT = GRID_OG_WIDTH * (1 / GRID_OG_RATIO);
|
||||
|
||||
const STORE_ID = process.env.BLOB_READ_WRITE_TOKEN?.match(
|
||||
/^vercel_blob_rw_([a-z0-9]+)_[a-z0-9]+$/i,
|
||||
)?.[1].toLowerCase();
|
||||
|
||||
export const BLOB_BASE_URL =
|
||||
`https://${STORE_ID}.public.blob.vercel-storage.com`;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import Link from 'next/link';
|
||||
import { pathForTag } from '@/site/paths';
|
||||
import { FaTag } from 'react-icons/fa';
|
||||
import { cc } from '@/utility/css';
|
||||
|
||||
export default function PhotoTag({
|
||||
tag,
|
||||
@ -15,7 +16,10 @@ export default function PhotoTag({
|
||||
>
|
||||
<FaTag
|
||||
size={11}
|
||||
className="text-gray-700 dark:text-gray-300 translate-y-[0.5px]"
|
||||
className={cc(
|
||||
'flex-shrink-0',
|
||||
'text-gray-700 dark:text-gray-300 translate-y-[0.5px]',
|
||||
)}
|
||||
/>
|
||||
<span className="uppercase">
|
||||
{tag.replaceAll('-', ' ')}
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
// Must be explicity defined next.config.js `imageSizes`
|
||||
export type NextImageWidth = 200 | 400 | 1050 | 1200;
|
||||
|
||||
export const getNextImageUrlForRequest = (
|
||||
imageUrl: string,
|
||||
request: Request,
|
||||
width: number,
|
||||
width: NextImageWidth,
|
||||
quality = 75,
|
||||
) => {
|
||||
const protocol = (request.headers.get('x-forwarded-proto') || 'https')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user