import { convertAudioToEndpointAudio } from "../collections/Audios/endpoints/getByID"; import { convertImageToEndpointImage } from "../collections/Images/endpoints/getByID"; import { convertRecorderToEndpointRecorderPreview } from "../collections/Recorders/endpoints/getByID"; import { convertVideoToEndpointVideo } from "../collections/Videos/endpoints/getByID"; import { AttributeTypes, RichTextBreakBlock, RichTextContent, RichTextSectionBlock, RichTextUploadNode, isBlockNodeBreakBlock, isBlockNodeSectionBlock, isNodeBlockNode, isNodeUploadNode, isUploadNodeAudioNode, isUploadNodeImageNode, isUploadNodeVideoNode, } from "../constants"; import { EndpointAttribute, EndpointCredit, EndpointPayloadImage, EndpointRole, EndpointScanImage, EndpointSource, EndpointSourcePreview, EndpointTag, PayloadImage, } from "../sdk"; import { Audio, Collectible, Credits, CreditsRole, Folder, Image, Language, MediaThumbnail, NumberBlock, Scan, Tag, TagsBlock, TextBlock, Video, } from "../types/collections"; import { isAudio, isDefined, isEmpty, isImage, isNotEmpty, isPayloadArrayType, isPayloadImage, isPayloadType, isPublished, isVideo, } from "./asserts"; const convertTagToEndpointTag = ({ id, slug, page, translations }: Tag): EndpointTag => ({ id, slug, ...(page && isPayloadType(page) ? { page: { slug: page.slug } } : {}), translations: translations.map(({ language, name }) => ({ language: isPayloadType(language) ? language.id : language, name, })), }); export const convertRTCToEndpointRTC = ( { root: { children, ...others } }: RichTextContent, parentPrefix = "" ): RichTextContent => { let index = 0; return { root: { ...others, children: children.map((node) => { if (isNodeBlockNode(node)) { // Add anchor hash on section block (TOC) if (isBlockNodeSectionBlock(node)) { index++; const anchorHash = `${parentPrefix}${index}.`; const newNode: RichTextSectionBlock = { ...node, fields: { ...node.fields, content: convertRTCToEndpointRTC(node.fields.content, anchorHash), }, anchorHash, }; return newNode; // Add anchor hash on section block (TOC) } else if (isBlockNodeBreakBlock(node)) { index++; const anchorHash = `${parentPrefix}${index}.`; const newNode: RichTextBreakBlock = { ...node, anchorHash, }; return newNode; } } else if (isNodeUploadNode(node)) { const errorUploadNode: RichTextUploadNode = { type: "upload", relationTo: "error", version: 1, }; if (isUploadNodeImageNode(node)) { const value = node.value as unknown as Image | string; if (!isImage(value)) return errorUploadNode; return { ...node, value: convertImageToEndpointImage(value), }; } else if (isUploadNodeAudioNode(node)) { const value = node.value as unknown as Audio | string; if (!isAudio(value)) return errorUploadNode; return { ...node, value: convertAudioToEndpointAudio(value), }; } else if (isUploadNodeVideoNode(node)) { const value = node.value as unknown as Video | string; if (!isVideo(value)) return errorUploadNode; return { ...node, value: convertVideoToEndpointVideo(value), }; } } return node; }), }, }; }; // TODO: Handle URL sources export const convertSourceToEndpointSource = ({ collectibles, folders, gallery, scans, }: { collectibles?: (string | Collectible)[] | null | undefined; scans?: (string | Collectible)[] | null | undefined; gallery?: (string | Collectible)[] | null | undefined; folders?: (string | Folder)[] | null | undefined; }): EndpointSource[] => { const result: EndpointSource[] = []; const convertFolderToEndpointSourcePreview = ({ id, slug, translations, }: Folder): EndpointSourcePreview => ({ id, slug, translations: translations.map(({ language, name }) => ({ language: isPayloadType(language) ? language.id : language, title: name, })), }); const convertCollectibleToEndpointSourcePreview = ({ id, slug, translations, }: Collectible): EndpointSourcePreview => ({ id, slug, translations: translations.map(({ language, title, pretitle, subtitle }) => ({ language: isPayloadType(language) ? language.id : language, title, ...(isNotEmpty(pretitle) ? { pretitle } : {}), ...(isNotEmpty(subtitle) ? { subtitle } : {}), })), }); if (collectibles && isPayloadArrayType(collectibles)) { collectibles.filter(isPublished).forEach((collectible) => { result.push({ type: "collectible", collectible: convertCollectibleToEndpointSourcePreview(collectible), }); }); } if (scans && isPayloadArrayType(scans)) { scans.filter(isPublished).forEach((collectible) => { result.push({ type: "scans", collectible: convertCollectibleToEndpointSourcePreview(collectible), }); }); } if (gallery && isPayloadArrayType(gallery)) { gallery.filter(isPublished).forEach((collectible) => { result.push({ type: "gallery", collectible: convertCollectibleToEndpointSourcePreview(collectible), }); }); } if (folders && isPayloadArrayType(folders)) { folders.forEach((folder) => { result.push({ type: "folder", folder: convertFolderToEndpointSourcePreview(folder), }); }); } return result; }; export const getDomainFromUrl = (url: string): string => { const urlObject = new URL(url); let domain = urlObject.hostname; if (domain.startsWith("www.")) { domain = domain.substring("www.".length); } return domain; }; export const getLanguageId = (language: string | Language) => typeof language === "object" ? language.id : language; const convertRoleToEndpointRole = ({ id, icon, translations }: CreditsRole): EndpointRole => ({ id, icon: icon ?? "material-symbols:person", translations: translations.map(({ language, name }) => ({ language: getLanguageId(language), name, })), }); export const convertCreditsToEndpointCredits = (credits?: Credits | null): EndpointCredit[] => credits?.flatMap<EndpointCredit>(({ recorders, role }) => { if (!isPayloadArrayType(recorders) || !isPayloadType(role)) return []; return [ { role: convertRoleToEndpointRole(role), recorders: recorders.map(convertRecorderToEndpointRecorderPreview), }, ]; }) ?? []; export const convertAttributesToEndpointAttributes = ( attributes: (TagsBlock | NumberBlock | TextBlock)[] | null | undefined ): EndpointAttribute[] => attributes?.map(convertAttributeToEndpointAttribute).filter(isDefined) ?? []; const convertAttributeToEndpointAttribute = ( attribute: TagsBlock | NumberBlock | TextBlock ): EndpointAttribute | undefined => { switch (attribute.blockType) { case "numberBlock": { const { name, number } = attribute; if (!isPayloadType(name)) return; const { id, slug, icon, translations } = name; return { id, slug, icon: icon ?? "material-symbols:category", translations: translations.map(({ language, name }) => ({ language: isPayloadType(language) ? language.id : language, name, })), type: AttributeTypes.Number, value: number, }; } case "textBlock": { const { name, text } = attribute; if (!isPayloadType(name)) return; if (isEmpty(text)) return; const { id, slug, icon, translations } = name; return { id, slug, icon: icon ?? "material-symbols:category", translations: translations.map(({ language, name }) => ({ language: isPayloadType(language) ? language.id : language, name, })), type: AttributeTypes.Text, value: text, }; } case "tagsBlock": { const { name, tags } = attribute; if (!isPayloadType(name)) return; if (!isPayloadArrayType(tags)) return; if (tags.length === 0) return; const { id, slug, icon, translations } = name; return { id, slug, icon: icon ?? "material-symbols:category", translations: translations.map(({ language, name }) => ({ language: isPayloadType(language) ? language.id : language, name, })), type: AttributeTypes.Tags, value: tags.map(convertTagToEndpointTag), }; } } }; type Nullable<T> = { [P in keyof T]?: T[P] | undefined | null }; export const convertSizesToPayloadImages = ( sizes: (Nullable<PayloadImage> | undefined)[], targetSizes: number[] ): PayloadImage[] => { const processedSizes = sizes.filter(isPayloadImage); const images: PayloadImage[] = []; 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 imagesAtTargetSize = processedSizes .filter(({ width }) => width > min && width <= max) .sort((a, b) => a.filesize - b.filesize); const smallestImage = imagesAtTargetSize[0]; if (!smallestImage) continue; images.push(smallestImage); } return images; }; export const convertScanToEndpointScanImage = ( { id, url, width, height, mimeType, filename, filesize, sizes }: Scan & PayloadImage, index: string ): EndpointScanImage => ({ id, index, url, width, height, filename, filesize, mimeType, sizes: convertSizesToPayloadImages( [ sizes?.["200w"], sizes?.["320w"], sizes?.["480w"], sizes?.["800w"], { url, width, height, filename, filesize, mimeType }, ], [200, 320, 480, 800] ), }); export const convertImageToEndpointPayloadImage = ({ url, width, height, mimeType, filename, filesize, id, sizes, }: Image & PayloadImage): EndpointPayloadImage => ({ filename, filesize, height, id, mimeType, sizes: convertSizesToPayloadImages( [ 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] ), url, width, ...(isPayloadImage(sizes?.og) ? { openGraph: sizes.og } : {}), }); export const convertMediaThumbnailToEndpointPayloadImage = ({ id, url, width, height, mimeType, filename, filesize, sizes, }: MediaThumbnail & PayloadImage): EndpointPayloadImage => ({ id, url, width, height, filename, filesize, mimeType, sizes: convertSizesToPayloadImages( [ 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] ), ...(isPayloadImage(sizes?.og) ? { openGraph: sizes.og } : {}), });