From a4ad2d0f21da413e4e45f4dcd18195bbdc80d3e1 Mon Sep 17 00:00:00 2001 From: DrMint <29893320+DrMint@users.noreply.github.com> Date: Wed, 22 May 2024 18:47:44 +0200 Subject: [PATCH] Image sizes generation --- src/collections/Audios/endpoints/getByID.ts | 5 +- .../endpoints/getBySlugEndpoint.ts | 26 ++-- .../endpoints/getBySlugEndpointScanPage.ts | 7 +- .../endpoints/getBySlugEndpointScans.ts | 97 ++++++++---- src/collections/Images/Images.ts | 26 ++-- src/collections/Images/endpoints/getByID.ts | 15 ++ .../MediaThumbnails/MediaThumbnails.ts | 25 ++-- src/collections/Scans/Scans.ts | 17 +-- src/collections/Videos/endpoints/getByID.ts | 5 +- .../WebsiteConfig/WebsiteConfig.ts | 23 +++ .../endpoints/getConfigEndpoint.ts | 45 ++++-- .../imageSizesRegenerationEndpoint.ts | 42 ++++++ src/fields/imageField/Cell.tsx | 24 ++- src/sdk.ts | 32 +++- src/types/collections.ts | 141 +++++++++++++++++- src/utils/asserts.ts | 29 ++++ src/utils/collectionConfig.ts | 4 +- src/utils/endpoints.ts | 96 ++++++++++++ src/utils/imageCollectionConfig.ts | 32 ++++ 19 files changed, 575 insertions(+), 116 deletions(-) create mode 100644 src/endpoints/imageSizesRegenerationEndpoint.ts diff --git a/src/collections/Audios/endpoints/getByID.ts b/src/collections/Audios/endpoints/getByID.ts index 32255e3..54891bf 100644 --- a/src/collections/Audios/endpoints/getByID.ts +++ b/src/collections/Audios/endpoints/getByID.ts @@ -7,6 +7,7 @@ import { isNotEmpty, isValidPayloadImage, isValidPayloadMedia } from "../../../u import { convertAttributesToEndpointAttributes, convertCreditsToEndpointCredits, + convertMediaThumbnailToEndpointMediaThumbnail, convertRTCToEndpointRTC, getLanguageId, } from "../../../utils/endpoints"; @@ -77,6 +78,8 @@ export const convertAudioToEndpointAudio = ({ ...(isNotEmpty(description) ? { description: convertRTCToEndpointRTC(description) } : {}), })) ?? [], duration, - ...(isValidPayloadImage(thumbnail) ? { thumbnail } : {}), + ...(isValidPayloadImage(thumbnail) + ? { thumbnail: convertMediaThumbnailToEndpointMediaThumbnail(thumbnail) } + : {}), credits: convertCreditsToEndpointCredits(credits), }); diff --git a/src/collections/Collectibles/endpoints/getBySlugEndpoint.ts b/src/collections/Collectibles/endpoints/getBySlugEndpoint.ts index 4069d2c..110eca4 100644 --- a/src/collections/Collectibles/endpoints/getBySlugEndpoint.ts +++ b/src/collections/Collectibles/endpoints/getBySlugEndpoint.ts @@ -13,6 +13,7 @@ import { } from "../../../utils/asserts"; import { convertAttributesToEndpointAttributes, + convertScanToEndpointScanImage, convertSourceToEndpointSource, getDomainFromUrl, } from "../../../utils/endpoints"; @@ -147,29 +148,34 @@ const handleGallery = (gallery: Collectible["gallery"]): EndpointCollectible["ga }; const handleScans = (scans: Collectible["scans"]): EndpointCollectible["scans"] => { - const result = - scans?.pages?.flatMap(({ image }) => { - if (!isValidPayloadImage(image)) return []; - return image; - }) ?? []; + if (!scans) return; const totalCount = Object.keys(scans?.cover ?? {}).length + Object.keys(scans?.dustjacket ?? {}).length + Object.keys(scans?.obi ?? {}).length + - result.length; + (scans.pages ?? []).length; + + const result = + scans?.pages?.flatMap(({ image, page }) => { + if (!isValidPayloadImage(image)) return []; + return { image, index: page.toString() }; + }) ?? []; if (isValidPayloadImage(scans?.cover?.front)) { - result.push(scans.cover.front); + result.push({ image: scans.cover.front, index: "cover-front" }); } if (isValidPayloadImage(scans?.dustjacket?.front)) { - result.push(scans.dustjacket.front); + result.push({ image: scans.dustjacket.front, index: "dustjacket-front" }); } const thumbnail = result?.[0]; - if (!thumbnail || !isValidPayloadImage(thumbnail)) return; - return { count: totalCount, thumbnail }; + if (!thumbnail || !isValidPayloadImage(thumbnail.image)) return; + return { + count: totalCount, + thumbnail: convertScanToEndpointScanImage(thumbnail.image, thumbnail.index), + }; }; const handleContents = (contents: Collectible["contents"]): EndpointCollectible["contents"] => { diff --git a/src/collections/Collectibles/endpoints/getBySlugEndpointScanPage.ts b/src/collections/Collectibles/endpoints/getBySlugEndpointScanPage.ts index 3644f24..96d4e24 100644 --- a/src/collections/Collectibles/endpoints/getBySlugEndpointScanPage.ts +++ b/src/collections/Collectibles/endpoints/getBySlugEndpointScanPage.ts @@ -4,7 +4,10 @@ import { EndpointCollectibleScanPage } from "../../../sdk"; import { Collectible, Scan } from "../../../types/collections"; import { CollectionEndpoint } from "../../../types/payload"; import { isDefined, isNotEmpty, isPayloadType, isValidPayloadImage } from "../../../utils/asserts"; -import { convertSourceToEndpointSource } from "../../../utils/endpoints"; +import { + convertScanToEndpointScanImage, + convertSourceToEndpointSource, +} from "../../../utils/endpoints"; import { convertImageToEndpointImage } from "../../Images/endpoints/getByID"; export const getBySlugEndpointScanPage: CollectionEndpoint = { @@ -51,7 +54,7 @@ export const getBySlugEndpointScanPage: CollectionEndpoint = { const nextIndex = getNextIndex(index, collectible.scans); const scanPage: EndpointCollectibleScanPage = { - image: { ...image, index }, + image: convertScanToEndpointScanImage(image, index), parentPages: convertSourceToEndpointSource({ scans: [collectible] }), slug, ...(isValidPayloadImage(collectible.thumbnail) diff --git a/src/collections/Collectibles/endpoints/getBySlugEndpointScans.ts b/src/collections/Collectibles/endpoints/getBySlugEndpointScans.ts index 9d6b693..e6035e5 100644 --- a/src/collections/Collectibles/endpoints/getBySlugEndpointScans.ts +++ b/src/collections/Collectibles/endpoints/getBySlugEndpointScans.ts @@ -5,6 +5,7 @@ import { Collectible } from "../../../types/collections"; import { isNotEmpty, isPayloadType, isValidPayloadImage } from "../../../utils/asserts"; import { convertCreditsToEndpointCredits, + convertScanToEndpointScanImage, convertSourceToEndpointSource, } from "../../../utils/endpoints"; import { convertImageToEndpointImage } from "../../Images/endpoints/getByID"; @@ -51,7 +52,7 @@ const handleScans = ({ credits: convertCreditsToEndpointCredits(credits), pages: pages?.flatMap(({ image, page }) => - isValidPayloadImage(image) ? { ...image, index: page.toString() } : [] + isValidPayloadImage(image) ? convertScanToEndpointScanImage(image, page.toString()) : [] ) ?? [], ...(coverEnabled && cover ? { cover: handleCover(cover) } : {}), ...(dustjacketEnabled && dustjacket ? { dustjacket: handleDustjacket(dustjacket) } : {}), @@ -69,25 +70,35 @@ const handleCover = ({ insideFront, spine, }: NonNullable["cover"]>): EndpointCollectibleScans["cover"] => ({ - ...(isValidPayloadImage(back) ? { back: { ...back, index: "cover-back" } } : {}), - ...(isValidPayloadImage(flapBack) ? { flapBack: { ...flapBack, index: "cover-flap-back" } } : {}), - ...(isValidPayloadImage(flapFront) - ? { flapFront: { ...flapFront, index: "cover-flap-front" } } + ...(isValidPayloadImage(back) + ? { back: convertScanToEndpointScanImage(back, "cover-back") } + : {}), + ...(isValidPayloadImage(flapBack) + ? { flapBack: convertScanToEndpointScanImage(flapBack, "cover-flap-back") } + : {}), + ...(isValidPayloadImage(flapFront) + ? { flapFront: convertScanToEndpointScanImage(flapFront, "cover-flap-front") } + : {}), + ...(isValidPayloadImage(front) + ? { front: convertScanToEndpointScanImage(front, "cover-front") } : {}), - ...(isValidPayloadImage(front) ? { front: { ...front, index: "cover-front" } } : {}), ...(isValidPayloadImage(insideBack) - ? { insideBack: { ...insideBack, index: "cover-inside-back" } } + ? { insideBack: convertScanToEndpointScanImage(insideBack, "cover-inside-back") } : {}), ...(isValidPayloadImage(insideFlapBack) - ? { insideFlapBack: { ...insideFlapBack, index: "cover-inside-flap-back" } } + ? { insideFlapBack: convertScanToEndpointScanImage(insideFlapBack, "cover-inside-flap-back") } : {}), ...(isValidPayloadImage(insideFlapFront) - ? { insideFlapFront: { ...insideFlapFront, index: "cover-inside-flap-front" } } + ? { + insideFlapFront: convertScanToEndpointScanImage(insideFlapFront, "cover-inside-flap-front"), + } : {}), ...(isValidPayloadImage(insideFront) - ? { insideFront: { ...insideFront, index: "cover-inside-front" } } + ? { insideFront: convertScanToEndpointScanImage(insideFront, "cover-inside-front") } + : {}), + ...(isValidPayloadImage(spine) + ? { spine: convertScanToEndpointScanImage(spine, "cover-spine") } : {}), - ...(isValidPayloadImage(spine) ? { spine: { ...spine, index: "cover-spine" } } : {}), }); const handleDustjacket = ({ @@ -104,29 +115,45 @@ const handleDustjacket = ({ }: NonNullable< NonNullable["dustjacket"] >): EndpointCollectibleScans["dustjacket"] => ({ - ...(isValidPayloadImage(back) ? { back: { ...back, index: "dustjacket-back" } } : {}), + ...(isValidPayloadImage(back) + ? { back: convertScanToEndpointScanImage(back, "dustjacket-back") } + : {}), ...(isValidPayloadImage(flapBack) - ? { flapBack: { ...flapBack, index: "dustjacket-flap-back" } } + ? { flapBack: convertScanToEndpointScanImage(flapBack, "dustjacket-flap-back") } : {}), ...(isValidPayloadImage(flapFront) - ? { flapFront: { ...flapFront, index: "dustjacket-flap-front" } } + ? { flapFront: convertScanToEndpointScanImage(flapFront, "dustjacket-flap-front") } + : {}), + ...(isValidPayloadImage(front) + ? { front: convertScanToEndpointScanImage(front, "dustjacket-front") } : {}), - ...(isValidPayloadImage(front) ? { front: { ...front, index: "dustjacket-front" } } : {}), ...(isValidPayloadImage(insideBack) - ? { insideBack: { ...insideBack, index: "dustjacket-inside-back" } } + ? { insideBack: convertScanToEndpointScanImage(insideBack, "dustjacket-inside-back") } : {}), ...(isValidPayloadImage(insideFlapBack) - ? { insideFlapBack: { ...insideFlapBack, index: "dustjacket-inside-flap-back" } } + ? { + insideFlapBack: convertScanToEndpointScanImage( + insideFlapBack, + "dustjacket-inside-flap-back" + ), + } : {}), ...(isValidPayloadImage(insideFlapFront) - ? { insideFlapFront: { ...insideFlapFront, index: "dustjacket-inside-flap-front" } } + ? { + insideFlapFront: convertScanToEndpointScanImage( + insideFlapFront, + "dustjacket-inside-flap-front" + ), + } : {}), ...(isValidPayloadImage(insideFront) - ? { insideFront: { ...insideFront, index: "dustjacket-inside-front" } } + ? { insideFront: convertScanToEndpointScanImage(insideFront, "dustjacket-inside-front") } + : {}), + ...(isValidPayloadImage(spine) + ? { spine: convertScanToEndpointScanImage(spine, "dustjacket-spine") } : {}), - ...(isValidPayloadImage(spine) ? { spine: { ...spine, index: "dustjacket-spine" } } : {}), ...(isValidPayloadImage(insideSpine) - ? { insideSpine: { ...insideSpine, index: "dustjacket-inside-spine" } } + ? { insideSpine: convertScanToEndpointScanImage(insideSpine, "dustjacket-inside-spine") } : {}), }); @@ -142,26 +169,32 @@ const handleObi = ({ insideSpine, spine, }: NonNullable["obi"]>): EndpointCollectibleScans["obi"] => ({ - ...(isValidPayloadImage(back) ? { back: { ...back, index: "obi-back" } } : {}), - ...(isValidPayloadImage(flapBack) ? { flapBack: { ...flapBack, index: "obi-flap-back" } } : {}), - ...(isValidPayloadImage(flapFront) - ? { flapFront: { ...flapFront, index: "obi-flap-front" } } + ...(isValidPayloadImage(back) ? { back: convertScanToEndpointScanImage(back, "obi-back") } : {}), + ...(isValidPayloadImage(flapBack) + ? { flapBack: convertScanToEndpointScanImage(flapBack, "obi-flap-back") } + : {}), + ...(isValidPayloadImage(flapFront) + ? { flapFront: convertScanToEndpointScanImage(flapFront, "obi-flap-front") } + : {}), + ...(isValidPayloadImage(front) + ? { front: convertScanToEndpointScanImage(front, "obi-front") } : {}), - ...(isValidPayloadImage(front) ? { front: { ...front, index: "obi-front" } } : {}), ...(isValidPayloadImage(insideBack) - ? { insideBack: { ...insideBack, index: "obi-inside-back" } } + ? { insideBack: convertScanToEndpointScanImage(insideBack, "obi-inside-back") } : {}), ...(isValidPayloadImage(insideFlapBack) - ? { insideFlapBack: { ...insideFlapBack, index: "obi-inside-flap-back" } } + ? { insideFlapBack: convertScanToEndpointScanImage(insideFlapBack, "obi-inside-flap-back") } : {}), ...(isValidPayloadImage(insideFlapFront) - ? { insideFlapFront: { ...insideFlapFront, index: "obi-inside-flap-front" } } + ? { insideFlapFront: convertScanToEndpointScanImage(insideFlapFront, "obi-inside-flap-front") } : {}), ...(isValidPayloadImage(insideFront) - ? { insideFront: { ...insideFront, index: "obi-inside-front" } } + ? { insideFront: convertScanToEndpointScanImage(insideFront, "obi-inside-front") } + : {}), + ...(isValidPayloadImage(spine) + ? { spine: convertScanToEndpointScanImage(spine, "obi-spine") } : {}), - ...(isValidPayloadImage(spine) ? { spine: { ...spine, index: "obi-spine" } } : {}), ...(isValidPayloadImage(insideSpine) - ? { insideSpine: { ...insideSpine, index: "obi-inside-spine" } } + ? { insideSpine: convertScanToEndpointScanImage(insideSpine, "obi-inside-spine") } : {}), }); diff --git a/src/collections/Images/Images.ts b/src/collections/Images/Images.ts index bc9307b..f832d8d 100644 --- a/src/collections/Images/Images.ts +++ b/src/collections/Images/Images.ts @@ -1,10 +1,15 @@ import { Collections } from "../../constants"; +import { createImageSizesRegenerationEndpoint } from "../../endpoints/imageSizesRegenerationEndpoint"; import { attributesField } from "../../fields/attributesField/attributesField"; import { creditsField } from "../../fields/creditsField/creditsField"; import { rowField } from "../../fields/rowField/rowField"; import { translatedFields } from "../../fields/translatedFields/translatedFields"; import { createEditor } from "../../utils/editor"; -import { buildImageCollectionConfig } from "../../utils/imageCollectionConfig"; +import { + buildImageCollectionConfig, + generateOpenGraphSize, + generateWebpSize, +} from "../../utils/imageCollectionConfig"; import { getByID } from "./endpoints/getByID"; const fields = { @@ -34,18 +39,17 @@ export const Images = buildImageCollectionConfig({ }, upload: { imageSizes: [ - { - name: "og", - height: 750, - width: 1125, - formatOptions: { - format: "jpg", - options: { progressive: true, mozjpeg: true, compressionLevel: 9, quality: 60 }, - }, - }, + generateOpenGraphSize(), + generateWebpSize(200, 60), + generateWebpSize(320, 60), + generateWebpSize(480, 70), + generateWebpSize(800, 70), + generateWebpSize(1280, 85), + generateWebpSize(1920, 85), + generateWebpSize(2560, 90), ], }, - endpoints: [getByID], + endpoints: [getByID, createImageSizesRegenerationEndpoint(Collections.Images)], fields: [ translatedFields({ name: fields.translations, diff --git a/src/collections/Images/endpoints/getByID.ts b/src/collections/Images/endpoints/getByID.ts index 472fae9..4db150d 100644 --- a/src/collections/Images/endpoints/getByID.ts +++ b/src/collections/Images/endpoints/getByID.ts @@ -8,6 +8,7 @@ import { convertAttributesToEndpointAttributes, convertCreditsToEndpointCredits, convertRTCToEndpointRTC, + convertSizesToEndpointImageSize, getLanguageId, } from "../../../utils/endpoints"; @@ -59,6 +60,7 @@ export const convertImageToEndpointImage = ({ filesize, id, credits, + sizes, }: Image & PayloadImage): EndpointImage => ({ url, width, @@ -79,4 +81,17 @@ export const convertImageToEndpointImage = ({ ...(isNotEmpty(description) ? { description: convertRTCToEndpointRTC(description) } : {}), })) ?? [], credits: convertCreditsToEndpointCredits(credits), + sizes: convertSizesToEndpointImageSize( + [ + sizes?.["200w"], + sizes?.["320w"], + sizes?.["480w"], + sizes?.["800w"], + sizes?.["1280w"], + sizes?.["1920w"], + sizes?.["2560w"], + { url, width, height, filename, filesize, mimeType }, + ], + [200, 320, 480, 800, 1280, 1920, 2560] + ), }); diff --git a/src/collections/MediaThumbnails/MediaThumbnails.ts b/src/collections/MediaThumbnails/MediaThumbnails.ts index e0caba0..c72ceef 100644 --- a/src/collections/MediaThumbnails/MediaThumbnails.ts +++ b/src/collections/MediaThumbnails/MediaThumbnails.ts @@ -1,6 +1,11 @@ import { shownOnlyToAdmin } from "../../accesses/collections/shownOnlyToAdmin"; import { Collections } from "../../constants"; -import { buildImageCollectionConfig } from "../../utils/imageCollectionConfig"; +import { createImageSizesRegenerationEndpoint } from "../../endpoints/imageSizesRegenerationEndpoint"; +import { + buildImageCollectionConfig, + generateOpenGraphSize, + generateWebpSize, +} from "../../utils/imageCollectionConfig"; const fields = { filename: "filename", @@ -16,17 +21,17 @@ export const MediaThumbnails = buildImageCollectionConfig({ plural: "Media Thumbnails", }, admin: { defaultColumns: [fields.filename, fields.updatedAt], hidden: shownOnlyToAdmin }, + endpoints: [createImageSizesRegenerationEndpoint(Collections.MediaThumbnails)], upload: { imageSizes: [ - { - name: "og", - height: 750, - width: 1125, - formatOptions: { - format: "jpg", - options: { progressive: true, mozjpeg: true, compressionLevel: 9, quality: 60 }, - }, - }, + generateOpenGraphSize(), + generateWebpSize(200, 60), + generateWebpSize(320, 60), + generateWebpSize(480, 70), + generateWebpSize(800, 70), + generateWebpSize(1280, 85), + generateWebpSize(1920, 85), + generateWebpSize(2560, 90), ], }, fields: [], diff --git a/src/collections/Scans/Scans.ts b/src/collections/Scans/Scans.ts index 470f06f..92fb517 100644 --- a/src/collections/Scans/Scans.ts +++ b/src/collections/Scans/Scans.ts @@ -1,6 +1,7 @@ import { shownOnlyToAdmin } from "../../accesses/collections/shownOnlyToAdmin"; import { Collections } from "../../constants"; -import { buildImageCollectionConfig } from "../../utils/imageCollectionConfig"; +import { createImageSizesRegenerationEndpoint } from "../../endpoints/imageSizesRegenerationEndpoint"; +import { buildImageCollectionConfig, generateWebpSize } from "../../utils/imageCollectionConfig"; const fields = { filename: "filename", @@ -19,17 +20,13 @@ export const Scans = buildImageCollectionConfig({ defaultColumns: [fields.filename, fields.updatedAt], hidden: shownOnlyToAdmin, }, + endpoints: [createImageSizesRegenerationEndpoint(Collections.Scans)], upload: { imageSizes: [ - { - name: "og", - height: 750, - width: 1125, - formatOptions: { - format: "jpg", - options: { progressive: true, mozjpeg: true, compressionLevel: 9, quality: 60 }, - }, - }, + generateWebpSize(200, 60), + generateWebpSize(320, 60), + generateWebpSize(480, 70), + generateWebpSize(800, 70), ], }, fields: [], diff --git a/src/collections/Videos/endpoints/getByID.ts b/src/collections/Videos/endpoints/getByID.ts index b4a20e2..98973a9 100644 --- a/src/collections/Videos/endpoints/getByID.ts +++ b/src/collections/Videos/endpoints/getByID.ts @@ -15,6 +15,7 @@ import { import { convertAttributesToEndpointAttributes, convertCreditsToEndpointCredits, + convertMediaThumbnailToEndpointMediaThumbnail, convertRTCToEndpointRTC, getLanguageId, } from "../../../utils/endpoints"; @@ -88,7 +89,9 @@ export const convertVideoToEndpointVideo = ({ })) ?? [], duration, - ...(isValidPayloadImage(thumbnail) ? { thumbnail } : {}), + ...(isValidPayloadImage(thumbnail) + ? { thumbnail: convertMediaThumbnailToEndpointMediaThumbnail(thumbnail) } + : {}), ...(platformEnabled && isDefined(platform) && isPayloadType(platform.channel) ? { platform: { diff --git a/src/collections/WebsiteConfig/WebsiteConfig.ts b/src/collections/WebsiteConfig/WebsiteConfig.ts index dde85fc..9ca1247 100644 --- a/src/collections/WebsiteConfig/WebsiteConfig.ts +++ b/src/collections/WebsiteConfig/WebsiteConfig.ts @@ -7,6 +7,9 @@ import { afterChangeWebhook } from "../../hooks/afterChangeWebhook"; import { getConfigEndpoint } from "./endpoints/getConfigEndpoint"; const fields = { + homeBackgroundImage: "homeBackgroundImage", + timelineBackgroundImage: "timelineBackgroundImage", + defaultOpenGraphImage: "defaultOpenGraphImage", homeFolders: "homeFolders", homeFoldersDarkThumbnail: "darkThumbnail", homeFoldersLightThumbnail: "lightThumbnail", @@ -32,6 +35,26 @@ export const WebsiteConfig: GlobalConfig = { afterChange: [afterChangeWebhook], }, fields: [ + rowField([ + { + name: fields.homeBackgroundImage, + type: "upload", + relationTo: Collections.Images, + required: true, + }, + { + name: fields.timelineBackgroundImage, + type: "upload", + relationTo: Collections.Images, + required: true, + }, + { + name: fields.defaultOpenGraphImage, + type: "upload", + relationTo: Collections.Images, + required: true, + }, + ]), { name: fields.homeFolders, admin: { diff --git a/src/collections/WebsiteConfig/endpoints/getConfigEndpoint.ts b/src/collections/WebsiteConfig/endpoints/getConfigEndpoint.ts index befcde5..29da9f8 100644 --- a/src/collections/WebsiteConfig/endpoints/getConfigEndpoint.ts +++ b/src/collections/WebsiteConfig/endpoints/getConfigEndpoint.ts @@ -20,7 +20,13 @@ export const getConfigEndpoint: CollectionEndpoint = { }); } - const { homeFolders, timeline } = await payload.findGlobal({ + const { + homeFolders, + timeline, + defaultOpenGraphImage, + homeBackgroundImage, + timelineBackgroundImage, + } = await payload.findGlobal({ slug: Collections.WebsiteConfig, }); @@ -43,20 +49,28 @@ export const getConfigEndpoint: CollectionEndpoint = { }); const endpointWebsiteConfig: EndpointWebsiteConfig = { - homeFolders: - homeFolders?.flatMap(({ folder, darkThumbnail, lightThumbnail }) => { - if (!isPayloadType(folder)) return []; - return { - ...convertFolderToEndpointFolder(folder), - ...(isValidPayloadImage(darkThumbnail) - ? { darkThumbnail: convertImageToEndpointImage(darkThumbnail) } - : {}), - ...(isValidPayloadImage(lightThumbnail) - ? { lightThumbnail: convertImageToEndpointImage(lightThumbnail) } - : {}), - }; - }) ?? [], + home: { + ...(isValidPayloadImage(homeBackgroundImage) + ? { backgroundImage: convertImageToEndpointImage(homeBackgroundImage) } + : {}), + folders: + homeFolders?.flatMap(({ folder, darkThumbnail, lightThumbnail }) => { + if (!isPayloadType(folder)) return []; + return { + ...convertFolderToEndpointFolder(folder), + ...(isValidPayloadImage(darkThumbnail) + ? { darkThumbnail: convertImageToEndpointImage(darkThumbnail) } + : {}), + ...(isValidPayloadImage(lightThumbnail) + ? { lightThumbnail: convertImageToEndpointImage(lightThumbnail) } + : {}), + }; + }) ?? [], + }, timeline: { + ...(isValidPayloadImage(timelineBackgroundImage) + ? { backgroundImage: convertImageToEndpointImage(timelineBackgroundImage) } + : {}), breaks: timeline?.breaks ?? [], eventCount, eras: @@ -69,6 +83,9 @@ export const getConfigEndpoint: CollectionEndpoint = { }; }) ?? [], }, + ...(isValidPayloadImage(defaultOpenGraphImage) + ? { defaultOpenGraphImage: convertImageToEndpointImage(defaultOpenGraphImage) } + : {}), }; res.status(200).json(endpointWebsiteConfig); }, diff --git a/src/endpoints/imageSizesRegenerationEndpoint.ts b/src/endpoints/imageSizesRegenerationEndpoint.ts new file mode 100644 index 0000000..039ddb6 --- /dev/null +++ b/src/endpoints/imageSizesRegenerationEndpoint.ts @@ -0,0 +1,42 @@ +import payload from "payload"; +import { CollectionEndpoint } from "../types/payload"; + +export const createImageSizesRegenerationEndpoint = ( + collection: "images" | "scans" | "media-thumbnails" +): CollectionEndpoint => ({ + path: `/regenerate`, + method: "get", + handler: async (req, res) => { + if (!req.user) { + return res.status(403).send({ + errors: [ + { + message: "You are not allowed to perform this action.", + }, + ], + }); + } + + const result = await payload.find({ + collection, + pagination: false, + }); + + for (const { id, filename } of result.docs) { + console.log("Handling", id); + + if (!filename) { + throw new Error("No filename!"); + } + + await payload.update({ + collection, + id, + filePath: `./uploads/${collection}/${filename}`, + data: {}, + }); + } + + res.status(200).send({ message: `Regenerated sizes for ${result.docs.length} images!` }); + }, +}); diff --git a/src/fields/imageField/Cell.tsx b/src/fields/imageField/Cell.tsx index e99c641..17ab1ff 100644 --- a/src/fields/imageField/Cell.tsx +++ b/src/fields/imageField/Cell.tsx @@ -1,22 +1,8 @@ import { Props } from "payload/components/views/Cell"; import React, { useEffect, useMemo, useState } from "react"; import { Link } from "react-router-dom"; -import styled from "styled-components"; import { isUndefined } from "../../utils/asserts"; -const Image = styled.img` - height: 3rem; - width: 3rem; - object-fit: contain; - transition: 0.2s transform; - transition-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1); - position: absolute; - transform: translateY(-50%) scale(1); - &:hover { - transform: translateY(-50%) scale(3); - } -`; - export const Cell = ({ cellData, field, rowData, collection }: Props): JSX.Element => { const [imageURL, setImageURL] = useState(); useEffect(() => { @@ -34,5 +20,13 @@ export const Cell = ({ cellData, field, rowData, collection }: Props): JSX.Eleme [collection.slug, rowData.id] ); - return {imageURL ? : ""}; + return ( + + {imageURL ? ( + + ) : ( + "" + )} + + ); }; diff --git a/src/sdk.ts b/src/sdk.ts index 5fd838e..a934cdd 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -53,11 +53,15 @@ export type EndpointFolder = { }; export type EndpointWebsiteConfig = { - homeFolders: (EndpointFolder & { - lightThumbnail?: EndpointImage; - darkThumbnail?: EndpointImage; - })[]; + home: { + backgroundImage?: EndpointImage; + folders: (EndpointFolder & { + lightThumbnail?: EndpointImage; + darkThumbnail?: EndpointImage; + })[]; + }; timeline: { + backgroundImage?: EndpointImage; breaks: number[]; eventCount: number; eras: { @@ -66,6 +70,7 @@ export type EndpointWebsiteConfig = { name: string; }[]; }; + defaultOpenGraphImage?: EndpointImage; }; export type EndpointRecorder = { @@ -176,7 +181,7 @@ export type EndpointCollectible = { backgroundImage?: EndpointImage; nature: CollectibleNature; gallery?: { count: number; thumbnail: EndpointImage }; - scans?: { count: number; thumbnail: PayloadImage }; + scans?: { count: number; thumbnail: EndpointScanImage }; urls: { url: string; label: string }[]; price?: { amount: number; @@ -341,6 +346,7 @@ export type EndpointCollectibleScanPage = { export type EndpointScanImage = PayloadImage & { index: string; + sizes: EndpointImageSize[]; }; export type TableOfContentEntry = { @@ -405,18 +411,26 @@ export type EndpointMedia = { credits: EndpointCredit[]; }; +export type EndpointImageSize = { + width: number; + height: number; + url: string; + wSize: number; +}; + export type EndpointImage = EndpointMedia & { width: number; height: number; + sizes: EndpointImageSize[]; }; export type EndpointAudio = EndpointMedia & { - thumbnail?: PayloadImage; + thumbnail?: EndpointMediaThumbnail; duration: number; }; export type EndpointVideo = EndpointMedia & { - thumbnail?: PayloadImage; + thumbnail?: EndpointMediaThumbnail; subtitles: { language: string; url: string; @@ -436,6 +450,10 @@ export type EndpointVideo = EndpointMedia & { duration: number; }; +export type EndpointMediaThumbnail = PayloadImage & { + sizes: EndpointImageSize[]; +}; + export type PayloadMedia = { url: string; mimeType: string; diff --git a/src/types/collections.ts b/src/types/collections.ts index 9c5df6d..30bbabf 100644 --- a/src/types/collections.ts +++ b/src/types/collections.ts @@ -159,6 +159,62 @@ export interface Image { filesize?: number | null; filename?: string | null; }; + "200w"?: { + url?: string | null; + width?: number | null; + height?: number | null; + mimeType?: string | null; + filesize?: number | null; + filename?: string | null; + }; + "320w"?: { + url?: string | null; + width?: number | null; + height?: number | null; + mimeType?: string | null; + filesize?: number | null; + filename?: string | null; + }; + "480w"?: { + url?: string | null; + width?: number | null; + height?: number | null; + mimeType?: string | null; + filesize?: number | null; + filename?: string | null; + }; + "800w"?: { + url?: string | null; + width?: number | null; + height?: number | null; + mimeType?: string | null; + filesize?: number | null; + filename?: string | null; + }; + "1280w"?: { + url?: string | null; + width?: number | null; + height?: number | null; + mimeType?: string | null; + filesize?: number | null; + filename?: string | null; + }; + "1920w"?: { + url?: string | null; + width?: number | null; + height?: number | null; + mimeType?: string | null; + filesize?: number | null; + filename?: string | null; + }; + "2560w"?: { + url?: string | null; + width?: number | null; + height?: number | null; + mimeType?: string | null; + filesize?: number | null; + filename?: string | null; + }; }; } /** @@ -573,7 +629,31 @@ export interface Scan { filesize?: number | null; filename?: string | null; }; - og?: { + "200w"?: { + url?: string | null; + width?: number | null; + height?: number | null; + mimeType?: string | null; + filesize?: number | null; + filename?: string | null; + }; + "320w"?: { + url?: string | null; + width?: number | null; + height?: number | null; + mimeType?: string | null; + filesize?: number | null; + filename?: string | null; + }; + "480w"?: { + url?: string | null; + width?: number | null; + height?: number | null; + mimeType?: string | null; + filesize?: number | null; + filename?: string | null; + }; + "800w"?: { url?: string | null; width?: number | null; height?: number | null; @@ -677,6 +757,62 @@ export interface MediaThumbnail { filesize?: number | null; filename?: string | null; }; + "200w"?: { + url?: string | null; + width?: number | null; + height?: number | null; + mimeType?: string | null; + filesize?: number | null; + filename?: string | null; + }; + "320w"?: { + url?: string | null; + width?: number | null; + height?: number | null; + mimeType?: string | null; + filesize?: number | null; + filename?: string | null; + }; + "480w"?: { + url?: string | null; + width?: number | null; + height?: number | null; + mimeType?: string | null; + filesize?: number | null; + filename?: string | null; + }; + "800w"?: { + url?: string | null; + width?: number | null; + height?: number | null; + mimeType?: string | null; + filesize?: number | null; + filename?: string | null; + }; + "1280w"?: { + url?: string | null; + width?: number | null; + height?: number | null; + mimeType?: string | null; + filesize?: number | null; + filename?: string | null; + }; + "1920w"?: { + url?: string | null; + width?: number | null; + height?: number | null; + mimeType?: string | null; + filesize?: number | null; + filename?: string | null; + }; + "2560w"?: { + url?: string | null; + width?: number | null; + height?: number | null; + mimeType?: string | null; + filesize?: number | null; + filename?: string | null; + }; }; } /** @@ -923,6 +1059,9 @@ export interface PayloadMigration { */ export interface WebsiteConfig { id: string; + homeBackgroundImage: string | Image; + timelineBackgroundImage: string | Image; + defaultOpenGraphImage: string | Image; homeFolders?: | { lightThumbnail?: string | Image | null; diff --git a/src/utils/asserts.ts b/src/utils/asserts.ts index 2d18e65..322c2d9 100644 --- a/src/utils/asserts.ts +++ b/src/utils/asserts.ts @@ -60,3 +60,32 @@ export const isPayloadArrayType = ( export const isPublished = ( object: T ): boolean => object._status === "published"; + +export type ImageSize = { + url?: string | null; + width?: number | null; + height?: number | null; + mimeType?: string | null; + filesize?: number | null; + filename?: string | null; +}; + +export type ValidImageSize = { + url: string; + width: number; + height: number; + mimeType: string; + filesize: number; + filename: string; +}; + +export const isValidImageSize = (size: ImageSize | undefined): size is ValidImageSize => { + if (isUndefined(size)) return false; + if (isUndefined(size.url)) return false; + if (isUndefined(size.width)) return false; + if (isUndefined(size.height)) return false; + if (isUndefined(size.mimeType)) return false; + if (isUndefined(size.filesize)) return false; + if (isUndefined(size.filename)) return false; + return true; +}; diff --git a/src/utils/collectionConfig.ts b/src/utils/collectionConfig.ts index 91695ba..44a1e70 100644 --- a/src/utils/collectionConfig.ts +++ b/src/utils/collectionConfig.ts @@ -1,5 +1,5 @@ +import { GeneratedTypes } from "payload"; import { CollectionConfig } from "payload/types"; -import { Collections } from "../constants"; import { formatToPascalCase } from "./string"; type CollectionConfigWithPlugins = CollectionConfig; @@ -8,7 +8,7 @@ export type BuildCollectionConfig = Omit< CollectionConfigWithPlugins, "slug" | "typescript" | "labels" > & { - slug: Collections; + slug: keyof GeneratedTypes["collections"]; labels: { singular: string; plural: string }; }; diff --git a/src/utils/endpoints.ts b/src/utils/endpoints.ts index d7825ef..12a7e5b 100644 --- a/src/utils/endpoints.ts +++ b/src/utils/endpoints.ts @@ -22,9 +22,13 @@ import { import { EndpointAttribute, EndpointCredit, + EndpointImageSize, + EndpointMediaThumbnail, EndpointRole, + EndpointScanImage, EndpointSource, EndpointTag, + PayloadImage, } from "../sdk"; import { Audio, @@ -34,18 +38,23 @@ import { Folder, Image, Language, + MediaThumbnail, NumberBlock, + Scan, Tag, TagsBlock, TextBlock, Video, } from "../types/collections"; import { + ImageSize, + ValidImageSize, isDefined, isEmpty, isPayloadArrayType, isPayloadType, isPublished, + isValidImageSize, isValidPayloadImage, isValidPayloadMedia, } from "./asserts"; @@ -272,3 +281,90 @@ const convertAttributeToEndpointAttribute = ( } } }; + +export const convertSizesToEndpointImageSize = ( + sizes: (ImageSize | undefined)[], + targetSizes: number[] +): EndpointImageSize[] => { + if (!sizes) return []; + const processedSizes = sizes.filter(isValidImageSize); + + const targetBins: { min: number; target: number; max: number; image: ValidImageSize }[] = []; + for (let index = 0; index < targetSizes.length; index++) { + const previous = targetSizes[index - 1]; + const current = targetSizes[index]!; + const next = targetSizes[index + 1]; + + const min = previous ? previous + (current - previous) / 2 : 0; + const max = next ? current + (next - current) / 2 : Infinity; + + const images = processedSizes + .filter(({ width }) => width > min && width <= max) + .sort((a, b) => a.filesize - b.filesize); + + const smallestImage = images[0]; + if (!smallestImage) continue; + + targetBins.push({ min, target: current, max, image: smallestImage }); + } + + return targetBins.map(({ target, image: { height, width, url } }) => ({ + width, + height, + url, + wSize: target, + })); +}; + +export const convertScanToEndpointScanImage = ( + { url, width, height, mimeType, filename, filesize, sizes }: Scan & PayloadImage, + index: string +): EndpointScanImage => ({ + index, + url, + width, + height, + filename, + filesize, + mimeType, + sizes: convertSizesToEndpointImageSize( + [ + sizes?.["200w"], + sizes?.["320w"], + sizes?.["480w"], + sizes?.["800w"], + { url, width, height, filename, filesize, mimeType }, + ], + [200, 320, 480, 800] + ), +}); + +export const convertMediaThumbnailToEndpointMediaThumbnail = ({ + url, + width, + height, + mimeType, + filename, + filesize, + sizes, +}: MediaThumbnail & PayloadImage): EndpointMediaThumbnail => ({ + url, + width, + height, + filename, + filesize, + mimeType, + sizes: convertSizesToEndpointImageSize( + [ + sizes?.["200w"], + sizes?.["320w"], + sizes?.["480w"], + sizes?.["800w"], + sizes?.["1280w"], + sizes?.["1920w"], + sizes?.["2560w"], + { url, width, height, filename, filesize, mimeType }, + ], + [200, 320, 480, 800, 1280, 1920, 2560] + ), +}); diff --git a/src/utils/imageCollectionConfig.ts b/src/utils/imageCollectionConfig.ts index 8137bd8..d668073 100644 --- a/src/utils/imageCollectionConfig.ts +++ b/src/utils/imageCollectionConfig.ts @@ -49,3 +49,35 @@ export const buildImageCollectionConfig = ({ ], }, }); + +export const generateOpenGraphSize = (): ImageSize => ({ + name: "og", + withoutEnlargement: true, + height: 1200, + width: 1200, + fit: "inside", + formatOptions: { + format: "jpg", + options: { + quality: 50, + optimizeScans: true, + quantizationTable: 2, + force: true, + }, + }, +}); + +export const generateWebpSize = (maxWidth: number, quality: number): ImageSize => ({ + name: `${maxWidth}w`, + withoutEnlargement: true, + width: maxWidth, + fit: "inside", + formatOptions: { + format: "webp", + options: { + quality, + alphaQuality: quality, + force: true, + }, + }, +});