diff --git a/src/collections/ContentFolders/ContentFolders.ts b/src/collections/ContentFolders/ContentFolders.ts new file mode 100644 index 0000000..80f2620 --- /dev/null +++ b/src/collections/ContentFolders/ContentFolders.ts @@ -0,0 +1,65 @@ +import { CollectionConfig } from "payload/types"; +import { slugField } from "../../fields/slugField/slugField"; +import { CollectionGroup, KeysTypes } from "../../constants"; +import { localizedFields } from "../../fields/translatedFields/translatedFields"; +import { collectionSlug } from "../../utils/string"; +import { Contents } from "../Contents/Contents"; + +const fields = { + slug: "slug", + translations: "translations", + name: "name", + subfolders: "subfolders", + contents: "contents", +} as const satisfies Record; + +const labels = { + singular: "Content Folder", + plural: "Content Folders", +} as const satisfies { singular: string; plural: string }; + +const slug = collectionSlug(labels.plural); + +export const ContentFolders: CollectionConfig = { + slug, + labels, + typescript: { interface: labels.singular }, + defaultSort: fields.slug, + admin: { + useAsTitle: fields.slug, + defaultColumns: [fields.slug, fields.translations], + group: CollectionGroup.Collections, + }, + timestamps: false, + versions: false, + fields: [ + slugField({ name: fields.slug }), + localizedFields({ + name: fields.translations, + interfaceName: "ContentFoldersTranslation", + admin: { + useAsTitle: fields.name, + }, + fields: [{ name: fields.name, type: "text", required: true }], + }), + { + type: "row", + fields: [ + { + type: "relationship", + name: fields.subfolders, + relationTo: [slug], + hasMany: true, + admin: { width: "50%" }, + }, + { + type: "relationship", + name: fields.contents, + relationTo: [Contents.slug], + hasMany: true, + admin: { width: "50%" }, + }, + ], + }, + ], +}; diff --git a/src/collections/ContentThumbnails/ContentThumbnails.ts b/src/collections/ContentThumbnails/ContentThumbnails.ts new file mode 100644 index 0000000..321228a --- /dev/null +++ b/src/collections/ContentThumbnails/ContentThumbnails.ts @@ -0,0 +1,52 @@ +import { CollectionConfig } from "payload/types"; +import { CollectionGroup } from "../../constants"; +import { collectionSlug } from "../../utils/string"; + +const fields = { + filename: "filename", + mimeType: "mimeType", + filesize: "filesize", +} as const satisfies Record; + +const labels = { + singular: "Content Thumbnail", + plural: "Content Thumbnails", +} as const satisfies { singular: string; plural: string }; + +export const ContentThumbnails: CollectionConfig = { + slug: collectionSlug(labels.plural), + labels, + typescript: { interface: labels.singular }, + defaultSort: fields.filename, + admin: { + useAsTitle: fields.filename, + group: CollectionGroup.Media, + }, + + upload: { + staticDir: `../uploads/${labels.plural}`, + mimeTypes: ["image/*"], + imageSizes: [ + { + name: "og", + height: 750, + width: 1125, + formatOptions: { + format: "jpg", + options: { progressive: true, mozjpeg: true, compressionLevel: 9, quality: 80 }, + }, + }, + { + name: "medium", + height: 1000, + width: 1500, + formatOptions: { + format: "webp", + options: { effort: 6, quality: 80, alphaQuality: 80 }, + }, + }, + ], + }, + + fields: [], +}; diff --git a/src/collections/Contents/Blocks/blocks.ts b/src/collections/Contents/Blocks/blocks.ts index c052ce5..0e4f4c8 100644 --- a/src/collections/Contents/Blocks/blocks.ts +++ b/src/collections/Contents/Blocks/blocks.ts @@ -1,169 +1,93 @@ -import { Block, BlockField } from "payload/types"; import { cueBlock } from "./cueBlock"; import { textBlock } from "./textBlock"; import { transcriptBlock } from "./transcriptBlock"; import { lineBlock } from "./lineBlock"; +import { quoteBlock } from "./quoteBlock"; +import { BlocksConfig, generateBlocks } from "../../../utils/recursiveBlocks"; -const INITIAL_DEPTH = 1; -const MAX_DEPTH = 4; - -enum BlockName { +const enum BlockName { Text = "Text", Section = "Section", Tabs = "Tabs", Tab = "Tab", - Columns = "Columns", - Column = "Column", Transcript = "Transcript", - Collapsible = "Collapsible", - Accordion = "Accordion", Line = "Line", Cue = "Cue", + Quote = "Quote", } -const rootBlocksNames: BlockName[] = [ - BlockName.Section, - BlockName.Collapsible, - BlockName.Columns, - BlockName.Tabs, - BlockName.Accordion, - BlockName.Text, - BlockName.Transcript, -]; - - -const recursiveBlocks: BlockName[] = [ - BlockName.Section, - BlockName.Collapsible, - BlockName.Accordion, - BlockName.Tabs, - BlockName.Tab, - BlockName.Column, - BlockName.Columns, -]; - -const blocksChildren: Record = { - Tabs: [BlockName.Tab], - Columns: [BlockName.Column], - Section: rootBlocksNames, - Collapsible: rootBlocksNames, - Tab: rootBlocksNames, - Column: rootBlocksNames, - Accordion: [BlockName.Collapsible], - Text: [], - Transcript: [BlockName.Line, BlockName.Cue], - Cue: [], - Line: [], -}; - -export type RecursiveBlock = Omit & { - fields: Omit & { - newDepth: (currentDepth: number) => number; - blocks: BlockName[]; - }; -}; - -// TODO: Check for loops in the block graph instead of manually defining recursive blocks -const isNotRecursiveBlock = (name: BlockName): boolean => !recursiveBlocks.includes(name); - -const implementationForRecursiveBlocks = ( - currentDepth: number, - { slug, interfaceName, labels, fields: { newDepth, blocks, ...fieldsProps } }: RecursiveBlock -): Block => ({ - slug: [slug, currentDepth].join("_"), - interfaceName: [interfaceName, currentDepth].join("_"), - labels, - fields: [ - { - ...fieldsProps, - type: "blocks", - blocks: blocks - .filter((block) => { - if (currentDepth < MAX_DEPTH) return true; - if (blocks.filter(isNotRecursiveBlock).length === 0) return true; - return isNotRecursiveBlock(block); - }) - .map((block) => implementations[block](newDepth(currentDepth))), - }, - ], -}); - -const implementations: Record Block> = { - Cue: () => cueBlock, - Text: () => textBlock, - Transcript: () => transcriptBlock, - Line: () => lineBlock, - Section: (currentDepth) => - implementationForRecursiveBlocks(currentDepth, { +const blocksConfig: BlocksConfig = { + Text: { + root: true, + block: textBlock, + }, + Section: { + root: true, + block: { slug: "section", - interfaceName: "Section", labels: { singular: "Section", plural: "Sections" }, - fields: { + recursion: { name: "content", + condition: (depth) => depth < 5, newDepth: (depth) => depth + 1, - blocks: blocksChildren.Section, + blocks: [ + BlockName.Section, + BlockName.Tabs, + BlockName.Transcript, + BlockName.Quote, + BlockName.Text, + ], }, - }), - Accordion: (currentDepth) => - implementationForRecursiveBlocks(currentDepth, { - slug: "accordion", - interfaceName: "Accordion", - labels: { singular: "Accordion", plural: "Accordions" }, - fields: { - name: "content", - newDepth: (depth) => depth + 1, - blocks: blocksChildren.Accordion, - }, - }), - Collapsible: (currentDepth) => - implementationForRecursiveBlocks(currentDepth, { - slug: "collapsible", - interfaceName: "Collapsible", - labels: { singular: "Collapsible", plural: "Collapsibles" }, - fields: { - name: "content", - newDepth: (depth) => depth + 1, - blocks: blocksChildren.Collapsible, - }, - }), - Tabs: (currentDepth) => - implementationForRecursiveBlocks(currentDepth, { + }, + }, + Tabs: { + root: true, + block: { slug: "tabs", - interfaceName: "Tabs", labels: { singular: "Tabs", plural: "Tabs" }, - fields: { name: "tabs", newDepth: (depth) => depth, blocks: blocksChildren.Tabs }, - }), - Tab: (currentDepth) => - implementationForRecursiveBlocks(currentDepth, { - slug: "tab", - interfaceName: "Tab", - labels: { singular: "Tab", plural: "Tabs" }, - fields: { - name: "content", - newDepth: (depth) => depth + 1, - blocks: blocksChildren.Tab, - }, - }), - Columns: (currentDepth) => - implementationForRecursiveBlocks(currentDepth, { - slug: "columns", - interfaceName: "Columns", - labels: { singular: "Columns", plural: "Columns" }, - fields: { - name: "columns", + recursion: { + name: "tabs", newDepth: (depth) => depth, - blocks: blocksChildren.Columns, + condition: (depth, parents) => !parents.includes(BlockName.Tabs) && depth < 5, + blocks: [BlockName.Tab], }, - }), - Column: (currentDepth) => - implementationForRecursiveBlocks(currentDepth, { - slug: "column", - interfaceName: "Column", - labels: { singular: "Column", plural: "Columns" }, - fields: { name: "content", newDepth: (depth) => depth + 1, blocks: blocksChildren.Column }, - }), + }, + }, + Tab: { + root: false, + block: { + slug: "tab", + labels: { singular: "Tab", plural: "Tabs" }, + recursion: { + name: "content", + condition: (depth) => depth < 5, + newDepth: (depth) => depth + 1, + blocks: [ + BlockName.Section, + BlockName.Tabs, + BlockName.Transcript, + BlockName.Quote, + BlockName.Text, + ], + }, + }, + }, + Transcript: { + root: true, + block: transcriptBlock, + }, + Cue: { + root: false, + block: cueBlock, + }, + Line: { + root: false, + block: lineBlock, + }, + Quote: { + root: true, + block: quoteBlock, + }, }; -export const rootBlocks: Block[] = rootBlocksNames - .filter((block) => block in implementations) - .map((block) => implementations[block](INITIAL_DEPTH)); +export const contentBlocks = generateBlocks(blocksConfig); diff --git a/src/collections/Contents/Blocks/quoteBlock.ts b/src/collections/Contents/Blocks/quoteBlock.ts new file mode 100644 index 0000000..a0c029e --- /dev/null +++ b/src/collections/Contents/Blocks/quoteBlock.ts @@ -0,0 +1,24 @@ +import { Block } from "payload/types"; + +export const quoteBlock: Block = { + slug: "quoteBlock", + interfaceName: "QuoteBlock", + labels: { singular: "Quote", plural: "Quotes" }, + fields: [ + { + name: "from", + type: "text", + required: true, + }, + { + name: "content", + type: "richText", + label: false, + required: true, + admin: { + hideGutter: true, + elements: [], + }, + }, + ], +}; diff --git a/src/collections/Contents/Contents.ts b/src/collections/Contents/Contents.ts index a7c2d2b..87acc73 100644 --- a/src/collections/Contents/Contents.ts +++ b/src/collections/Contents/Contents.ts @@ -1,14 +1,15 @@ import { CollectionConfig } from "payload/types"; import { collectionSlug } from "../../utils/string"; -import { CollectionGroup, FileTypes, TagsTypes } from "../../constants"; +import { CollectionGroup, FileTypes, KeysTypes } from "../../constants"; import { slugField } from "../../fields/slugField/slugField"; import { imageField } from "../../fields/imageField/imageField"; -import { Tags } from "../Tags/Tags"; +import { Keys } from "../Keys/Keys"; import { localizedFields } from "../../fields/translatedFields/translatedFields"; import { Recorders } from "../Recorders/Recorders"; import { isDefined } from "../../utils/asserts"; import { fileField } from "../../fields/fileField/fileField"; -import { rootBlocks } from "./Blocks/blocks"; +import { contentBlocks } from "./Blocks/blocks"; +import { ContentThumbnails } from "../ContentThumbnails/ContentThumbnails"; const fields = { slug: "slug", @@ -29,6 +30,7 @@ const fields = { videoNotes: "videoNotes", audio: "audio", audioNotes: "videoNotes", + status: "status", } as const satisfies Record; const labels = { @@ -43,42 +45,47 @@ export const Contents: CollectionConfig = { defaultSort: fields.slug, admin: { useAsTitle: fields.slug, - defaultColumns: [fields.slug, fields.thumbnail, fields.categories], + defaultColumns: [ + fields.slug, + fields.thumbnail, + fields.categories, + fields.type, + fields.translations, + fields.status, + ], group: CollectionGroup.Collections, preview: (doc) => `https://accords-library.com/contents/${doc.slug}`, }, timestamps: true, - versions: { drafts: true }, + versions: { drafts: { autosave: true } }, fields: [ { type: "row", fields: [ slugField({ name: fields.slug, admin: { width: "50%" } }), - imageField({ name: fields.thumbnail, admin: { width: "50%" } }), + imageField({ + name: fields.thumbnail, + relationTo: ContentThumbnails.slug, + admin: { width: "50%" }, + }), ], }, - { - name: "testing", - type: "blocks", - admin: { initCollapsed: true }, - blocks: rootBlocks, - }, { type: "row", fields: [ { name: fields.categories, type: "relationship", - relationTo: [Tags.slug], - filterOptions: { type: { equals: TagsTypes.Categories } }, + relationTo: [Keys.slug], + filterOptions: { type: { equals: KeysTypes.Categories } }, hasMany: true, admin: { allowCreate: false, width: "50%" }, }, { name: fields.type, type: "relationship", - relationTo: [Tags.slug], - filterOptions: { type: { equals: TagsTypes.Contents } }, + relationTo: [Keys.slug], + filterOptions: { type: { equals: KeysTypes.Contents } }, admin: { allowCreate: false, width: "50%" }, }, ], @@ -148,8 +155,10 @@ export const Contents: CollectionConfig = { { name: fields.textContent, label: "Content", - type: "richText", - admin: { hideGutter: true }, + labels: { singular: "Block", plural: "Blocks" }, + type: "blocks", + admin: { initCollapsed: true }, + blocks: contentBlocks, }, { name: fields.textNotes, diff --git a/src/collections/Tags/Tags.ts b/src/collections/Keys/Keys.ts similarity index 55% rename from src/collections/Tags/Tags.ts rename to src/collections/Keys/Keys.ts index 4ded066..d4f57a8 100644 --- a/src/collections/Tags/Tags.ts +++ b/src/collections/Keys/Keys.ts @@ -1,22 +1,27 @@ import { CollectionConfig } from "payload/types"; import { slugField } from "../../fields/slugField/slugField"; -import { CollectionGroup, TagsTypes } from "../../constants"; +import { CollectionGroup, KeysTypes } from "../../constants"; import { localizedFields } from "../../fields/translatedFields/translatedFields"; import { collectionSlug } from "../../utils/string"; +import { Key } from "../../types/collections"; +import { isDefined } from "../../utils/asserts"; const fields = { slug: "slug", translations: "translations", type: "type", name: "name", + short: "short", } as const satisfies Record; const labels = { - singular: "Tag", - plural: "Tags", + singular: "Key", + plural: "Keys", } as const satisfies { singular: string; plural: string }; -export const Tags: CollectionConfig = { +const keysTypesWithShort: (keyof typeof KeysTypes)[] = ["Categories", "GamePlatforms"]; + +export const Keys: CollectionConfig = { slug: collectionSlug(labels.plural), labels, typescript: { interface: labels.singular }, @@ -34,7 +39,7 @@ export const Tags: CollectionConfig = { name: fields.type, type: "select", required: true, - options: Object.entries(TagsTypes).map(([value, label]) => ({ label, value })), + options: Object.entries(KeysTypes).map(([value, label]) => ({ label, value })), }, localizedFields({ name: fields.translations, @@ -42,7 +47,23 @@ export const Tags: CollectionConfig = { admin: { useAsTitle: fields.name, }, - fields: [{ name: fields.name, type: "text", required: true }], + fields: [ + { + type: "row", + fields: [ + { name: fields.name, type: "text", required: true, admin: { width: "50%" } }, + { + name: fields.short, + type: "text", + admin: { + condition: (data: Partial) => + isDefined(data.type) && keysTypesWithShort.includes(data.type), + width: "50%", + }, + }, + ], + }, + ], }), ], }; diff --git a/src/collections/LibraryItemThumbnails/LibraryItemThumbnails.ts b/src/collections/LibraryItemThumbnails/LibraryItemThumbnails.ts new file mode 100644 index 0000000..a1041ee --- /dev/null +++ b/src/collections/LibraryItemThumbnails/LibraryItemThumbnails.ts @@ -0,0 +1,64 @@ +import { CollectionConfig } from "payload/types"; +import { CollectionGroup } from "../../constants"; +import { collectionSlug } from "../../utils/string"; + +const fields = { + filename: "filename", + mimeType: "mimeType", + filesize: "filesize", +} as const satisfies Record; + +const labels = { + singular: "Library Item Thumbnail", + plural: "Library Item Thumbnails", +} as const satisfies { singular: string; plural: string }; + +export const LibraryItemThumbnails: CollectionConfig = { + slug: collectionSlug(labels.plural), + labels, + typescript: { interface: labels.singular }, + defaultSort: fields.filename, + admin: { + useAsTitle: fields.filename, + group: CollectionGroup.Media, + }, + + upload: { + staticDir: `../uploads/${labels.plural}`, + mimeTypes: ["image/*"], + imageSizes: [ + { + name: "og", + height: 1024, + width: 1024, + fit: "contain", + formatOptions: { + format: "jpg", + options: { progressive: true, mozjpeg: true, compressionLevel: 9, quality: 80 }, + }, + }, + { + name: "medium", + height: 1024, + width: 1024, + fit: "contain", + formatOptions: { + format: "webp", + options: { effort: 6, quality: 80, alphaQuality: 80 }, + }, + }, + { + name: "large", + height: 2048, + width: 2048, + fit: "contain", + formatOptions: { + format: "webp", + options: { effort: 6, quality: 80, alphaQuality: 80 }, + }, + }, + ], + }, + + fields: [], +}; diff --git a/src/collections/LibraryItems/LibraryItems.ts b/src/collections/LibraryItems/LibraryItems.ts index 364f440..117c4d1 100644 --- a/src/collections/LibraryItems/LibraryItems.ts +++ b/src/collections/LibraryItems/LibraryItems.ts @@ -1,9 +1,19 @@ import { CollectionConfig } from "payload/types"; -import { CollectionGroup } from "../../constants"; +import { + CollectionGroup, + KeysTypes, + LibraryItemsTextualBindingTypes, + LibraryItemsTextualPageOrders, + LibraryItemsTypes, +} from "../../constants"; import { slugField } from "../../fields/slugField/slugField"; import { imageField } from "../../fields/imageField/imageField"; import { collectionSlug } from "../../utils/string"; import { isDefined, isUndefined } from "../../utils/asserts"; +import { LibraryItemThumbnails } from "../LibraryItemThumbnails/LibraryItemThumbnails"; +import { LibraryItem } from "../../types/collections"; +import { Keys } from "../Keys/Keys"; +import { Languages } from "../Languages"; const fields = { status: "status", @@ -20,6 +30,16 @@ const fields = { width: "width", height: "height", thickness: "thickness", + releaseDate: "releaseDate", + itemType: "itemType", + textual: "textual", + textualSubtype: "subtype", + textualBindingType: "bindingType", + textualPageCount: "pageCount", + textualPageOrder: "pageOrder", + textualLanguages: "languages", + audio: "audio", + audioSubtype: "audioSubtype", } as const satisfies Record; const labels = { @@ -50,13 +70,17 @@ export const LibraryItems: CollectionConfig = { preview: (doc) => `https://accords-library.com/library/${doc.slug}`, }, timestamps: true, - versions: { drafts: true }, + versions: { drafts: { autosave: true } }, fields: [ { type: "row", fields: [ slugField({ name: fields.slug, admin: { width: "50%" } }), - imageField({ name: fields.thumbnail, admin: { width: "50%" } }), + imageField({ + name: fields.thumbnail, + relationTo: LibraryItemThumbnails.slug, + admin: { width: "50%" }, + }), ], }, { @@ -144,5 +168,106 @@ export const LibraryItems: CollectionConfig = { }, ], }, + { + name: fields.itemType, + type: "radio", + options: Object.entries(LibraryItemsTypes).map(([value, label]) => ({ label, value })), + admin: { + layout: "horizontal", + }, + }, + { + name: fields.textual, + type: "group", + admin: { + condition: (data: Partial) => data.itemType === LibraryItemsTypes.Textual, + }, + fields: [ + { + type: "row", + fields: [ + { + name: fields.textualSubtype, + label: "Subtype", + type: "relationship", + relationTo: [Keys.slug], + filterOptions: { type: { equals: KeysTypes.LibraryTextual } }, + hasMany: true, + admin: { allowCreate: false, width: "50%" }, + }, + { + name: fields.textualLanguages, + type: "relationship", + relationTo: [Languages.slug], + hasMany: true, + admin: { allowCreate: false, width: "50%" }, + }, + ], + }, + { + type: "row", + fields: [ + { name: fields.textualPageCount, type: "number", min: 1, admin: { width: "33%" } }, + { + name: fields.textualBindingType, + label: "Binding Type", + type: "radio", + options: Object.entries(LibraryItemsTextualBindingTypes).map(([value, label]) => ({ + label, + value, + })), + admin: { + layout: "horizontal", + width: "33%", + }, + }, + { + name: fields.textualPageOrder, + label: "Page Order", + type: "radio", + options: Object.entries(LibraryItemsTextualPageOrders).map(([value, label]) => ({ + label, + value, + })), + admin: { + layout: "horizontal", + width: "33%", + }, + }, + ], + }, + ], + }, + { + name: fields.audio, + type: "group", + admin: { + condition: (data: Partial) => data.itemType === LibraryItemsTypes.Audio, + }, + fields: [ + { + type: "row", + fields: [ + { + name: fields.audioSubtype, + label: "Subtype", + type: "relationship", + relationTo: [Keys.slug], + filterOptions: { type: { equals: KeysTypes.LibraryAudio } }, + hasMany: true, + admin: { allowCreate: false, width: "50%" }, + }, + ], + }, + ], + }, + { + name: fields.releaseDate, + type: "date", + admin: { + date: { pickerAppearance: "dayOnly", displayFormat: "yyyy-MM-dd" }, + position: "sidebar", + }, + }, ], }; diff --git a/src/collections/Images/Images.ts b/src/collections/PostThumbnails/PostThumbnails.ts similarity index 53% rename from src/collections/Images/Images.ts rename to src/collections/PostThumbnails/PostThumbnails.ts index 56b9a33..2f7b58d 100644 --- a/src/collections/Images/Images.ts +++ b/src/collections/PostThumbnails/PostThumbnails.ts @@ -6,15 +6,14 @@ const fields = { filename: "filename", mimeType: "mimeType", filesize: "filesize", - alt: "alt", } as const satisfies Record; const labels = { - singular: "Image", - plural: "Images", + singular: "Post Thumbnail", + plural: "Post Thumbnails", } as const satisfies { singular: string; plural: string }; -export const Images: CollectionConfig = { +export const PostThumbnails: CollectionConfig = { slug: collectionSlug(labels.plural), labels, typescript: { interface: labels.singular }, @@ -27,13 +26,27 @@ export const Images: CollectionConfig = { upload: { staticDir: `../uploads/${labels.plural}`, mimeTypes: ["image/*"], + imageSizes: [ + { + name: "og", + height: 750, + width: 1125, + formatOptions: { + format: "jpg", + options: { progressive: true, mozjpeg: true, compressionLevel: 9, quality: 80 }, + }, + }, + { + name: "medium", + height: 1000, + width: 1500, + formatOptions: { + format: "webp", + options: { effort: 6, quality: 80, alphaQuality: 80 }, + }, + }, + ], }, - fields: [ - { - name: fields.alt, - label: "Alt Text", - type: "text", - }, - ], + fields: [], }; diff --git a/src/collections/Posts/Posts.ts b/src/collections/Posts/Posts.ts index 1317fdf..a79d20e 100644 --- a/src/collections/Posts/Posts.ts +++ b/src/collections/Posts/Posts.ts @@ -1,16 +1,18 @@ import { CollectionConfig } from "payload/types"; import { slugField } from "../../fields/slugField/slugField"; import { imageField } from "../../fields/imageField/imageField"; -import { CollectionGroup, TagsTypes } from "../../constants"; +import { CollectionGroup, KeysTypes } from "../../constants"; import { Recorders } from "../Recorders/Recorders"; import { localizedFields } from "../../fields/translatedFields/translatedFields"; import { isDefined, isUndefined } from "../../utils/asserts"; import { removeTranslatorsForTranscripts } from "./hooks/beforeValidate"; -import { Tags } from "../Tags/Tags"; +import { Keys } from "../Keys/Keys"; import { collectionSlug } from "../../utils/string"; +import { PostThumbnails } from "../PostThumbnails/PostThumbnails"; const fields = { slug: "slug", + hidden: "hidden", thumbnail: "thumbnail", categories: "categories", authors: "authors", @@ -44,13 +46,17 @@ export const Posts: CollectionConfig = { beforeValidate: [removeTranslatorsForTranscripts], }, timestamps: true, - versions: { drafts: true }, + versions: { drafts: { autosave: true } }, fields: [ { type: "row", fields: [ slugField({ name: fields.slug, admin: { width: "50%" } }), - imageField({ name: fields.thumbnail, admin: { width: "50%" } }), + imageField({ + name: fields.thumbnail, + relationTo: PostThumbnails.slug, + admin: { width: "50%" }, + }), ], }, { @@ -63,15 +69,15 @@ export const Posts: CollectionConfig = { required: true, minRows: 1, hasMany: true, - admin: { width: "50%" }, + admin: { width: "35%" }, }, { name: fields.categories, type: "relationship", - relationTo: [Tags.slug], - filterOptions: { type: { equals: TagsTypes.Categories } }, + relationTo: [Keys.slug], + filterOptions: { type: { equals: KeysTypes.Categories } }, hasMany: true, - admin: { allowCreate: false, width: "50%" }, + admin: { allowCreate: false, width: "35%" }, }, ], }, @@ -138,5 +144,15 @@ export const Posts: CollectionConfig = { }, required: true, }, + { + name: fields.hidden, + type: "checkbox", + required: false, + defaultValue: false, + admin: { + description: "If enabled, the post won't appear in the 'News' section", + position: "sidebar", + }, + }, ], }; diff --git a/src/collections/RecorderThumbnails/RecorderThumbnails.ts b/src/collections/RecorderThumbnails/RecorderThumbnails.ts new file mode 100644 index 0000000..74449eb --- /dev/null +++ b/src/collections/RecorderThumbnails/RecorderThumbnails.ts @@ -0,0 +1,53 @@ +import { CollectionConfig } from "payload/types"; +import { CollectionGroup } from "../../constants"; +import { collectionSlug } from "../../utils/string"; + +const fields = { + filename: "filename", + mimeType: "mimeType", + filesize: "filesize", +} as const satisfies Record; + +const labels = { + singular: "Recorder Thumbnail", + plural: "Recorder Thumbnails", +} as const satisfies { singular: string; plural: string }; + +export const RecorderThumbnails: CollectionConfig = { + slug: collectionSlug(labels.plural), + labels, + typescript: { interface: labels.singular }, + defaultSort: fields.filename, + admin: { + useAsTitle: fields.filename, + group: CollectionGroup.Media, + }, + + upload: { + staticDir: `../uploads/${labels.plural}`, + adminThumbnail: "small", + mimeTypes: ["image/*"], + imageSizes: [ + { + name: "og", + height: 256, + width: 256, + formatOptions: { + format: "jpg", + options: { progressive: true, mozjpeg: true, compressionLevel: 9, quality: 80 }, + }, + }, + { + name: "small", + height: 128, + width: 128, + formatOptions: { + format: "webp", + options: { effort: 6, quality: 80, alphaQuality: 80 }, + }, + }, + ], + }, + + fields: [], +}; diff --git a/src/collections/Recorders/Recorders.ts b/src/collections/Recorders/Recorders.ts index 1a295b4..0102a32 100644 --- a/src/collections/Recorders/Recorders.ts +++ b/src/collections/Recorders/Recorders.ts @@ -1,12 +1,11 @@ import { CollectionConfig } from "payload/types"; import { localizedFields } from "../../fields/translatedFields/translatedFields"; import { Languages } from "../Languages"; -import { Images } from "../Images/Images"; -import { Cell } from "../../fields/imageField/Cell"; import { beforeDuplicate } from "./hooks/beforeDuplicate"; -import { imageField } from "../../fields/imageField/imageField"; import { CollectionGroup } from "../../constants"; import { collectionSlug } from "../../utils/string"; +import { RecorderThumbnails } from "../RecorderThumbnails/RecorderThumbnails"; +import { imageField } from "../../fields/imageField/imageField"; const fields = { username: "username", @@ -32,7 +31,13 @@ export const Recorders: CollectionConfig = { hooks: { beforeDuplicate }, description: "Recorders are contributors of the Accord's Library project. Create a Recorder here to be able to credit them in other collections", - defaultColumns: [fields.username, fields.anonymize, fields.biographies, fields.languages], + defaultColumns: [ + fields.username, + fields.avatar, + fields.anonymize, + fields.biographies, + fields.languages, + ], group: CollectionGroup.Meta, }, timestamps: false, @@ -40,25 +45,18 @@ export const Recorders: CollectionConfig = { { type: "row", fields: [ - imageField({ name: fields.avatar }), { name: fields.username, type: "text", unique: true, required: true, - admin: { description: "The username must be unique" }, - }, - { - name: fields.anonymize, - type: "checkbox", - required: true, - defaultValue: false, - admin: { - width: "50%", - description: - "If enabled, this recorder's username will not be made public. Instead they will be referred to as 'Recorder#0000' where '0000' is a random four digit number", - }, + admin: { description: "The username must be unique", width: "33%" }, }, + imageField({ + name: fields.avatar, + relationTo: RecorderThumbnails.slug, + admin: { width: "66%" }, + }), ], }, { @@ -81,5 +79,16 @@ export const Recorders: CollectionConfig = { }, fields: [{ name: fields.biography, type: "textarea" }], }), + { + name: fields.anonymize, + type: "checkbox", + required: true, + defaultValue: false, + admin: { + description: + "If enabled, this recorder's username will not be made public. Instead they will be referred to as 'Recorder#0000' where '0000' is a random four digit number", + position: "sidebar", + }, + }, ], }; diff --git a/src/constants.ts b/src/constants.ts index 360d235..b475ac5 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -5,7 +5,7 @@ export enum CollectionGroup { Administration = "Administration", } -export enum TagsTypes { +export enum KeysTypes { Contents = "Contents", LibraryAudio = "Library / Audio", LibraryVideo = "Library / Video", @@ -15,6 +15,7 @@ export enum TagsTypes { Weapons = "Weapons", GamePlatforms = "Game Platforms", Categories = "Categories", + Wordings = "Wordings", } export enum FileTypes { @@ -23,3 +24,21 @@ export enum FileTypes { ContentVideo = "Content / Video", ContentAudio = "Content / Audio", } + +export enum LibraryItemsTypes { + Textual = "Textual", + Audio = "Audio", + Video = "Video", + Game = "Game", + Other = "Other", +} + +export enum LibraryItemsTextualBindingTypes { + Paperback = "Paperback", + Hardcover = "Hardcover", +} + +export enum LibraryItemsTextualPageOrders { + LeftToRight = "Left to right", + RightToLeft = "Right to left", +} diff --git a/src/fields/imageField/imageField.ts b/src/fields/imageField/imageField.ts index 61b2af4..c15962c 100644 --- a/src/fields/imageField/imageField.ts +++ b/src/fields/imageField/imageField.ts @@ -1,13 +1,11 @@ import { UploadField } from "payload/types"; -import { Images } from "../../collections/Images/Images"; import { Cell } from "./Cell"; -type Props = Omit; +type Props = Omit; export const imageField = ({ admin, ...otherProps }: Props): UploadField => ({ ...otherProps, type: "upload", - relationTo: Images.slug, admin: { components: { Cell, diff --git a/src/payload.config.ts b/src/payload.config.ts index c6d0c95..d80e768 100644 --- a/src/payload.config.ts +++ b/src/payload.config.ts @@ -3,12 +3,16 @@ import path from "path"; import { Users } from "./collections/Users"; import { Languages } from "./collections/Languages"; import { Recorders } from "./collections/Recorders/Recorders"; -import { Images } from "./collections/Images/Images"; import { Posts } from "./collections/Posts/Posts"; -import { Tags } from "./collections/Tags/Tags"; +import { Keys } from "./collections/Keys/Keys"; import { LibraryItems } from "./collections/LibraryItems/LibraryItems"; import { Contents } from "./collections/Contents/Contents"; import { Files } from "./collections/Files/Files"; +import { RecorderThumbnails } from "./collections/RecorderThumbnails/RecorderThumbnails"; +import { PostThumbnails } from "./collections/PostThumbnails/PostThumbnails"; +import { LibraryItemThumbnails } from "./collections/LibraryItemThumbnails/LibraryItemThumbnails"; +import { ContentThumbnails } from "./collections/ContentThumbnails/ContentThumbnails"; +import { ContentFolders } from "./collections/ContentFolders/ContentFolders"; import { Logo } from "./components/Logo"; import { Icon } from "./components/Icon"; @@ -21,15 +25,30 @@ export default buildConfig({ favicon: "/public/favicon.ico", ogImage: "og.jpg", titleSuffix: "- Accord’s Library", - }, + }, css: path.resolve(__dirname, "styles.scss"), }, + collections: [ + LibraryItems, + Contents, + ContentFolders, + Posts, + ContentThumbnails, + LibraryItemThumbnails, + RecorderThumbnails, + PostThumbnails, + Files, + Languages, + Recorders, + Keys, + Users, + ], globals: [], telemetry: false, typescript: { outputFile: path.resolve(__dirname, "types/collections.ts"), }, graphQL: { - schemaOutputFile: path.resolve(__dirname, "generated-schema.graphql"), + disable: true, }, }); diff --git a/src/types/collections.ts b/src/types/collections.ts index 35f16de..d9631a4 100644 --- a/src/types/collections.ts +++ b/src/types/collections.ts @@ -9,6 +9,7 @@ export type CategoryTranslations = { language: string | Language; name: string; + short?: string; id?: string; }[]; export type RecorderBiographies = { @@ -16,25 +17,39 @@ export type RecorderBiographies = { biography?: string; id?: string; }[]; +export type ContentFoldersTranslation = { + language: string | Language; + name: string; + id?: string; +}[]; export interface Config { collections: { 'library-items': LibraryItem; contents: Content; + 'content-folders': ContentFolder; posts: Post; - images: Image; + 'content-thumbnails': ContentThumbnail; + 'library-item-thumbnails': LibraryItemThumbnail; + 'recorder-thumbnails': RecorderThumbnail; + 'post-thumbnails': PostThumbnail; files: File; languages: Language; recorders: Recorder; - tags: Tag; + keys: Key; users: User; }; globals: {}; } export interface LibraryItem { id: string; + /** + * @minItems 2 + * @maxItems 2 + */ + test?: [number, number]; slug: string; - thumbnail?: string | Image; + thumbnail?: string | LibraryItemThumbnail; pretitle?: string; title: string; subtitle?: string; @@ -47,13 +62,37 @@ export interface LibraryItem { height?: number; thickness?: number; }; + itemType?: 'Textual' | 'Audio' | 'Video' | 'Game' | 'Other'; + textual?: { + subtype?: + | { + value: string; + relationTo: 'keys'; + }[] + | { + value: Key; + relationTo: 'keys'; + }[]; + languages?: + | { + value: string; + relationTo: 'languages'; + }[] + | { + value: Language; + relationTo: 'languages'; + }[]; + pageCount?: number; + bindingType?: 'Paperback' | 'Hardcover'; + pageOrder?: 'LeftToRight' | 'RightToLeft'; + }; + releaseDate?: string; updatedAt: string; createdAt: string; _status?: 'draft' | 'published'; } -export interface Image { +export interface LibraryItemThumbnail { id: string; - alt?: string; updatedAt: string; createdAt: string; url?: string; @@ -62,24 +101,69 @@ export interface Image { filesize?: number; width?: number; height?: number; + sizes?: { + og?: { + url?: string; + width?: number; + height?: number; + mimeType?: string; + filesize?: number; + filename?: string; + }; + medium?: { + url?: string; + width?: number; + height?: number; + mimeType?: string; + filesize?: number; + filename?: string; + }; + large?: { + url?: string; + width?: number; + height?: number; + mimeType?: string; + filesize?: number; + filename?: string; + }; + }; +} +export interface Key { + id: string; + slug: string; + type: + | 'Contents' + | 'LibraryAudio' + | 'LibraryVideo' + | 'LibraryTextual' + | 'LibraryGroup' + | 'Library' + | 'Weapons' + | 'GamePlatforms' + | 'Categories' + | 'Wordings'; + translations?: CategoryTranslations; +} +export interface Language { + id: string; + name: string; } export interface Content { id: string; slug: string; - thumbnail?: string | Image; - testing?: (Section_1 | Collapsible_1 | Columns_1 | Tabs_1 | Accordion_1 | TextBlock | TranscriptBlock)[]; + thumbnail?: string | ContentThumbnail; categories?: | { value: string; - relationTo: 'tags'; + relationTo: 'keys'; }[] | { - value: Tag; - relationTo: 'tags'; + value: Key; + relationTo: 'keys'; }[]; type?: { - value: string | Tag; - relationTo: 'tags'; + value: string | Key; + relationTo: 'keys'; }; translations: { language: string | Language; @@ -91,9 +175,7 @@ export interface Content { textTranscribers?: string[] | Recorder[]; textTranslators?: string[] | Recorder[]; textProofreaders?: string[] | Recorder[]; - textContent?: { - [k: string]: unknown; - }[]; + textContent?: (TextBlock | Section | Tabs | TranscriptBlock | QuoteBlock)[]; textNotes?: string; video?: string | File; videoNotes?: string; @@ -104,29 +186,71 @@ export interface Content { createdAt: string; _status?: 'draft' | 'published'; } -export interface Section_1 { - content?: (Section_2 | Collapsible_2 | Columns_2 | Tabs_2 | Accordion_2 | TextBlock | TranscriptBlock)[]; - id?: string; - blockName?: string; - blockType: 'section_1'; +export interface ContentThumbnail { + id: string; + updatedAt: string; + createdAt: string; + url?: string; + filename?: string; + mimeType?: string; + filesize?: number; + width?: number; + height?: number; + sizes?: { + og?: { + url?: string; + width?: number; + height?: number; + mimeType?: string; + filesize?: number; + filename?: string; + }; + medium?: { + url?: string; + width?: number; + height?: number; + mimeType?: string; + filesize?: number; + filename?: string; + }; + }; } -export interface Section_2 { - content?: (Section_3 | Collapsible_3 | Columns_3 | Tabs_3 | Accordion_3 | TextBlock | TranscriptBlock)[]; - id?: string; - blockName?: string; - blockType: 'section_2'; +export interface Recorder { + id: string; + username: string; + avatar?: string | RecorderThumbnail; + languages?: string[] | Language[]; + biographies?: RecorderBiographies; + anonymize: boolean; } -export interface Section_3 { - content?: (Section_4 | Collapsible_4 | Columns_4 | Tabs_4 | Accordion_4 | TextBlock | TranscriptBlock)[]; - id?: string; - blockName?: string; - blockType: 'section_3'; -} -export interface Section_4 { - content?: (TextBlock | TranscriptBlock)[]; - id?: string; - blockName?: string; - blockType: 'section_4'; +export interface RecorderThumbnail { + id: string; + updatedAt: string; + createdAt: string; + url?: string; + filename?: string; + mimeType?: string; + filesize?: number; + width?: number; + height?: number; + sizes?: { + og?: { + url?: string; + width?: number; + height?: number; + mimeType?: string; + filesize?: number; + filename?: string; + }; + small?: { + url?: string; + width?: number; + height?: number; + mimeType?: string; + filesize?: number; + filename?: string; + }; + }; } export interface TextBlock { content: { @@ -136,6 +260,42 @@ export interface TextBlock { blockName?: string; blockType: 'textBlock'; } +export interface Section { + content?: (Section_Section | Section_Tabs | TranscriptBlock | QuoteBlock | TextBlock)[]; + id?: string; + blockName?: string; + blockType: 'section'; +} +export interface Section_Section { + content?: (Section_Section_Section | Section_Section_Tabs | TranscriptBlock | QuoteBlock | TextBlock)[]; + id?: string; + blockName?: string; + blockType: 'section'; +} +export interface Section_Section_Section { + content?: ( + | Section_Section_Section_Section + | Section_Section_Section_Tabs + | TranscriptBlock + | QuoteBlock + | TextBlock + )[]; + id?: string; + blockName?: string; + blockType: 'section'; +} +export interface Section_Section_Section_Section { + content?: (Section_Section_Section_Section_Section | TranscriptBlock | QuoteBlock | TextBlock)[]; + id?: string; + blockName?: string; + blockType: 'section'; +} +export interface Section_Section_Section_Section_Section { + content?: (TranscriptBlock | QuoteBlock | TextBlock)[]; + id?: string; + blockName?: string; + blockType: 'section'; +} export interface TranscriptBlock { lines: (LineBlock | CueBlock)[]; id?: string; @@ -156,182 +316,122 @@ export interface CueBlock { blockName?: string; blockType: 'cueBlock'; } -export interface Collapsible_4 { - content?: (TextBlock | TranscriptBlock)[]; +export interface QuoteBlock { + from: string; + content: { + [k: string]: unknown; + }[]; id?: string; blockName?: string; - blockType: 'collapsible_4'; + blockType: 'quoteBlock'; } -export interface Columns_4 { - columns?: Column_4[]; +export interface Section_Section_Section_Tabs { + tabs?: Section_Section_Section_Tabs_Tab[]; id?: string; blockName?: string; - blockType: 'columns_4'; + blockType: 'tabs'; } -export interface Column_4 { - content?: (TextBlock | TranscriptBlock)[]; +export interface Section_Section_Section_Tabs_Tab { + content?: (Section_Section_Section_Tabs_Tab_Section | TranscriptBlock | QuoteBlock | TextBlock)[]; id?: string; blockName?: string; - blockType: 'column_4'; + blockType: 'tab'; } -export interface Tabs_4 { - tabs?: Tab_4[]; +export interface Section_Section_Section_Tabs_Tab_Section { + content?: (TranscriptBlock | QuoteBlock | TextBlock)[]; id?: string; blockName?: string; - blockType: 'tabs_4'; + blockType: 'section'; } -export interface Tab_4 { - content?: (TextBlock | TranscriptBlock)[]; +export interface Section_Section_Tabs { + tabs?: Section_Section_Tabs_Tab[]; id?: string; blockName?: string; - blockType: 'tab_4'; + blockType: 'tabs'; } -export interface Accordion_4 { - content?: Collapsible_5[]; +export interface Section_Section_Tabs_Tab { + content?: (Section_Section_Tabs_Tab_Section | TranscriptBlock | QuoteBlock | TextBlock)[]; id?: string; blockName?: string; - blockType: 'accordion_4'; + blockType: 'tab'; } -export interface Collapsible_5 { - content?: (TextBlock | TranscriptBlock)[]; +export interface Section_Section_Tabs_Tab_Section { + content?: (Section_Section_Tabs_Tab_Section_Section | TranscriptBlock | QuoteBlock | TextBlock)[]; id?: string; blockName?: string; - blockType: 'collapsible_5'; + blockType: 'section'; } -export interface Collapsible_3 { - content?: (Section_4 | Collapsible_4 | Columns_4 | Tabs_4 | Accordion_4 | TextBlock | TranscriptBlock)[]; +export interface Section_Section_Tabs_Tab_Section_Section { + content?: (TranscriptBlock | QuoteBlock | TextBlock)[]; id?: string; blockName?: string; - blockType: 'collapsible_3'; + blockType: 'section'; } -export interface Columns_3 { - columns?: Column_3[]; +export interface Section_Tabs { + tabs?: Section_Tabs_Tab[]; id?: string; blockName?: string; - blockType: 'columns_3'; + blockType: 'tabs'; } -export interface Column_3 { - content?: (Section_4 | Collapsible_4 | Columns_4 | Tabs_4 | Accordion_4 | TextBlock | TranscriptBlock)[]; +export interface Section_Tabs_Tab { + content?: (Section_Tabs_Tab_Section | TranscriptBlock | QuoteBlock | TextBlock)[]; id?: string; blockName?: string; - blockType: 'column_3'; + blockType: 'tab'; } -export interface Tabs_3 { - tabs?: Tab_3[]; +export interface Section_Tabs_Tab_Section { + content?: (Section_Tabs_Tab_Section_Section | TranscriptBlock | QuoteBlock | TextBlock)[]; id?: string; blockName?: string; - blockType: 'tabs_3'; + blockType: 'section'; } -export interface Tab_3 { - content?: (Section_4 | Collapsible_4 | Columns_4 | Tabs_4 | Accordion_4 | TextBlock | TranscriptBlock)[]; +export interface Section_Tabs_Tab_Section_Section { + content?: (Section_Tabs_Tab_Section_Section_Section | TranscriptBlock | QuoteBlock | TextBlock)[]; id?: string; blockName?: string; - blockType: 'tab_3'; + blockType: 'section'; } -export interface Accordion_3 { - content?: Collapsible_4[]; +export interface Section_Tabs_Tab_Section_Section_Section { + content?: (TranscriptBlock | QuoteBlock | TextBlock)[]; id?: string; blockName?: string; - blockType: 'accordion_3'; + blockType: 'section'; } -export interface Collapsible_2 { - content?: (Section_3 | Collapsible_3 | Columns_3 | Tabs_3 | Accordion_3 | TextBlock | TranscriptBlock)[]; +export interface Tabs { + tabs?: Tabs_Tab[]; id?: string; blockName?: string; - blockType: 'collapsible_2'; + blockType: 'tabs'; } -export interface Columns_2 { - columns?: Column_2[]; +export interface Tabs_Tab { + content?: (Tabs_Tab_Section | TranscriptBlock | QuoteBlock | TextBlock)[]; id?: string; blockName?: string; - blockType: 'columns_2'; + blockType: 'tab'; } -export interface Column_2 { - content?: (Section_3 | Collapsible_3 | Columns_3 | Tabs_3 | Accordion_3 | TextBlock | TranscriptBlock)[]; +export interface Tabs_Tab_Section { + content?: (Tabs_Tab_Section_Section | TranscriptBlock | QuoteBlock | TextBlock)[]; id?: string; blockName?: string; - blockType: 'column_2'; + blockType: 'section'; } -export interface Tabs_2 { - tabs?: Tab_2[]; +export interface Tabs_Tab_Section_Section { + content?: (Tabs_Tab_Section_Section_Section | TranscriptBlock | QuoteBlock | TextBlock)[]; id?: string; blockName?: string; - blockType: 'tabs_2'; + blockType: 'section'; } -export interface Tab_2 { - content?: (Section_3 | Collapsible_3 | Columns_3 | Tabs_3 | Accordion_3 | TextBlock | TranscriptBlock)[]; +export interface Tabs_Tab_Section_Section_Section { + content?: (Tabs_Tab_Section_Section_Section_Section | TranscriptBlock | QuoteBlock | TextBlock)[]; id?: string; blockName?: string; - blockType: 'tab_2'; + blockType: 'section'; } -export interface Accordion_2 { - content?: Collapsible_3[]; +export interface Tabs_Tab_Section_Section_Section_Section { + content?: (TranscriptBlock | QuoteBlock | TextBlock)[]; id?: string; blockName?: string; - blockType: 'accordion_2'; -} -export interface Collapsible_1 { - content?: (Section_2 | Collapsible_2 | Columns_2 | Tabs_2 | Accordion_2 | TextBlock | TranscriptBlock)[]; - id?: string; - blockName?: string; - blockType: 'collapsible_1'; -} -export interface Columns_1 { - columns?: Column_1[]; - id?: string; - blockName?: string; - blockType: 'columns_1'; -} -export interface Column_1 { - content?: (Section_2 | Collapsible_2 | Columns_2 | Tabs_2 | Accordion_2 | TextBlock | TranscriptBlock)[]; - id?: string; - blockName?: string; - blockType: 'column_1'; -} -export interface Tabs_1 { - tabs?: Tab_1[]; - id?: string; - blockName?: string; - blockType: 'tabs_1'; -} -export interface Tab_1 { - content?: (Section_2 | Collapsible_2 | Columns_2 | Tabs_2 | Accordion_2 | TextBlock | TranscriptBlock)[]; - id?: string; - blockName?: string; - blockType: 'tab_1'; -} -export interface Accordion_1 { - content?: Collapsible_2[]; - id?: string; - blockName?: string; - blockType: 'accordion_1'; -} -export interface Tag { - id: string; - slug: string; - type: - | 'Contents' - | 'LibraryAudio' - | 'LibraryVideo' - | 'LibraryTextual' - | 'LibraryGroup' - | 'Library' - | 'Weapons' - | 'GamePlatforms' - | 'Categories'; - translations?: CategoryTranslations; -} -export interface Language { - id: string; - name: string; -} -export interface Recorder { - id: string; - avatar?: string | Image; - username: string; - anonymize: boolean; - languages?: string[] | Language[]; - biographies?: RecorderBiographies; + blockType: 'section'; } export interface File { id: string; @@ -340,10 +440,33 @@ export interface File { updatedAt: string; createdAt: string; } +export interface ContentFolder { + id: string; + slug: string; + translations?: ContentFoldersTranslation; + subfolders?: + | { + value: string; + relationTo: 'content-folders'; + }[] + | { + value: ContentFolder; + relationTo: 'content-folders'; + }[]; + contents?: + | { + value: string; + relationTo: 'contents'; + }[] + | { + value: Content; + relationTo: 'contents'; + }[]; +} export interface Post { id: string; slug: string; - thumbnail?: string | Image; + thumbnail?: string | PostThumbnail; authors: | { value: string; @@ -356,11 +479,11 @@ export interface Post { categories?: | { value: string; - relationTo: 'tags'; + relationTo: 'keys'; }[] | { - value: Tag; - relationTo: 'tags'; + value: Key; + relationTo: 'keys'; }[]; translations: { language: string | Language; @@ -375,10 +498,40 @@ export interface Post { id?: string; }[]; publishedDate: string; + hidden?: boolean; updatedAt: string; createdAt: string; _status?: 'draft' | 'published'; } +export interface PostThumbnail { + id: string; + updatedAt: string; + createdAt: string; + url?: string; + filename?: string; + mimeType?: string; + filesize?: number; + width?: number; + height?: number; + sizes?: { + og?: { + url?: string; + width?: number; + height?: number; + mimeType?: string; + filesize?: number; + filename?: string; + }; + medium?: { + url?: string; + width?: number; + height?: number; + mimeType?: string; + filesize?: number; + filename?: string; + }; + }; +} export interface User { id: string; email: string; diff --git a/src/utils/recursiveBlocks.ts b/src/utils/recursiveBlocks.ts new file mode 100644 index 0000000..8980ac2 --- /dev/null +++ b/src/utils/recursiveBlocks.ts @@ -0,0 +1,95 @@ +import { Block, BlockField } from "payload/types"; + +const isDefined = (value: T | null | undefined): value is T => + value !== null && value !== undefined; + +const recursionFieldName = "recursion" as const; + +type BlockConfig = { + root: boolean; + block: RecursiveBlock | Block; +}; + +type RecursiveBlock = Omit & { + [recursionFieldName]: Omit & { + newDepth: (currentDepth: number) => number; + condition: (currentDepth: number, parents: T[]) => boolean; + blocks: T[]; + }; + fields?: Block["fields"]; +}; + +export type BlocksConfig = Record>; + +export const generateBlocks = (blocksConfig: BlocksConfig): Block[] => { + const isRecursiveBlock = (block: RecursiveBlock | Block): block is RecursiveBlock => + recursionFieldName in block; + + const getInterfaceName = (parents: T[], currentBlockName: T): string => { + const capitalize = (text: string): string => { + if (text.length === 0) return text; + const [firstLetter, ...rest] = text; + return [firstLetter.toUpperCase(), ...rest].join(""); + }; + return [...parents, currentBlockName] + .map((blockName) => blocksConfig[blockName].block.slug) + .map(capitalize) + .join("_"); + }; + + const getCurrentDepth = (parents: T[]): number => + parents.reduce((acc, blockName) => { + const block = blocksConfig[blockName].block; + if (!isRecursiveBlock(block)) return acc; + return block[recursionFieldName].newDepth(acc); + }, 1); + + const generateRecursiveBlocks = (parents: T[], blockName: T): Block | undefined => { + const currentDepth = getCurrentDepth(parents); + const block = blocksConfig[blockName].block; + if (!isRecursiveBlock(block)) return block; + + const { + slug, + labels, + fields = [], + recursion: { newDepth, blocks, condition, ...fieldsProps }, + } = block; + + const generatedBlocks = blocks + .filter((blockName) => { + const block = blocksConfig[blockName].block; + if (!isRecursiveBlock(block)) return true; + return block[recursionFieldName].condition(currentDepth, parents); + }) + .map((nextBlockName) => generateRecursiveBlocks([...parents, blockName], nextBlockName)) + .filter(isDefined); + + // Cut dead branches (branches without leafs) + if (generatedBlocks.length === 0) { + return undefined; + } + + return { + slug, + interfaceName: getInterfaceName(parents, blockName), + labels, + fields: [ + ...fields, + { + ...fieldsProps, + type: "blocks", + blocks: generatedBlocks, + }, + ], + }; + }; + + const rootBlockNames = Object.entries>(blocksConfig) + .filter(([_, blockConfig]) => blockConfig.root) + .map(([blockName]) => blockName as T); + + return rootBlockNames + .map((blockName) => generateRecursiveBlocks([], blockName)) + .filter(isDefined); +}; diff --git a/tsconfig.json b/tsconfig.json index 7bf8e87..0f9e073 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,8 @@ { "compilerOptions": { - "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "target": "ES2022", + "moduleResolution": "NodeNext", + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "strict": false, "esModuleInterop": true, @@ -14,21 +11,13 @@ "rootDir": "./src", "jsx": "react", "paths": { - "payload/generated-types": [ - "./src/payload-types.ts", - ], + "payload/generated-types": ["./src/payload-types.ts"] } }, - "include": [ - "src" - ], - "exclude": [ - "node_modules", - "dist", - "build", - ], + "include": ["src"], + "exclude": ["node_modules", "dist", "build"], "ts-node": { "transpileOnly": true, - "swc": true, + "swc": true } }