diff --git a/src/collections/ContentFolders/ContentFolders.ts b/src/collections/ContentFolders/ContentFolders.ts index 80f2620..ff3acb7 100644 --- a/src/collections/ContentFolders/ContentFolders.ts +++ b/src/collections/ContentFolders/ContentFolders.ts @@ -1,9 +1,8 @@ -import { CollectionConfig } from "payload/types"; import { slugField } from "../../fields/slugField/slugField"; -import { CollectionGroup, KeysTypes } from "../../constants"; +import { CollectionGroup } from "../../constants"; import { localizedFields } from "../../fields/translatedFields/translatedFields"; -import { collectionSlug } from "../../utils/string"; import { Contents } from "../Contents/Contents"; +import { buildCollectionConfig } from "../../utils/collectionConfig"; const fields = { slug: "slug", @@ -13,53 +12,49 @@ const fields = { 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, +export const ContentFolders = buildCollectionConfig( + { + singular: "Content Folder", + plural: "Content Folders", }, - 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%" }, - }, - ], + ({ slug }) => ({ + 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 index 321228a..fcd29a6 100644 --- a/src/collections/ContentThumbnails/ContentThumbnails.ts +++ b/src/collections/ContentThumbnails/ContentThumbnails.ts @@ -1,6 +1,5 @@ -import { CollectionConfig } from "payload/types"; import { CollectionGroup } from "../../constants"; -import { collectionSlug } from "../../utils/string"; +import { buildCollectionConfig } from "../../utils/collectionConfig"; const fields = { filename: "filename", @@ -8,45 +7,41 @@ const fields = { 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, +export const ContentThumbnails = buildCollectionConfig( + { + singular: "Content Thumbnail", + plural: "Content Thumbnails", }, - - 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 }, + ({ labels }) => ({ + 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 }, + { + name: "medium", + height: 1000, + width: 1500, + formatOptions: { + format: "webp", + options: { effort: 6, quality: 80, alphaQuality: 80 }, + }, }, - }, - ], - }, - - fields: [], -}; + ], + }, + fields: [], + }) +); diff --git a/src/collections/Contents/Contents.ts b/src/collections/Contents/Contents.ts index 87acc73..d8e848b 100644 --- a/src/collections/Contents/Contents.ts +++ b/src/collections/Contents/Contents.ts @@ -1,5 +1,3 @@ -import { CollectionConfig } from "payload/types"; -import { collectionSlug } from "../../utils/string"; import { CollectionGroup, FileTypes, KeysTypes } from "../../constants"; import { slugField } from "../../fields/slugField/slugField"; import { imageField } from "../../fields/imageField/imageField"; @@ -10,6 +8,7 @@ import { isDefined } from "../../utils/asserts"; import { fileField } from "../../fields/fileField/fileField"; import { contentBlocks } from "./Blocks/blocks"; import { ContentThumbnails } from "../ContentThumbnails/ContentThumbnails"; +import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig"; const fields = { slug: "slug", @@ -31,187 +30,186 @@ const fields = { audio: "audio", audioNotes: "videoNotes", status: "status", + updatedBy: "updatedBy", } as const satisfies Record; -const labels = { - singular: "Content", - plural: "Contents", -} as const satisfies { singular: string; plural: string }; - -export const Contents: CollectionConfig = { - slug: collectionSlug(labels.plural), - labels, - typescript: { interface: labels.singular }, - defaultSort: fields.slug, - admin: { - useAsTitle: fields.slug, - 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}`, +export const Contents = buildVersionedCollectionConfig( + { + singular: "Content", + plural: "Contents", }, - timestamps: true, - versions: { drafts: { autosave: true } }, - fields: [ - { - type: "row", - fields: [ - slugField({ name: fields.slug, admin: { width: "50%" } }), - imageField({ - name: fields.thumbnail, - relationTo: ContentThumbnails.slug, - admin: { width: "50%" }, - }), + () => ({ + defaultSort: fields.slug, + admin: { + useAsTitle: fields.slug, + description: + "All the contents (textual, audio, and video) from the Library or other online sources.", + 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}`, }, - { - type: "row", - fields: [ - { - name: fields.categories, - type: "relationship", - relationTo: [Keys.slug], - filterOptions: { type: { equals: KeysTypes.Categories } }, - hasMany: true, - admin: { allowCreate: false, width: "50%" }, - }, - { - name: fields.type, - type: "relationship", - relationTo: [Keys.slug], - filterOptions: { type: { equals: KeysTypes.Contents } }, - admin: { allowCreate: false, width: "50%" }, - }, - ], - }, - localizedFields({ - name: fields.translations, - admin: { useAsTitle: fields.title, hasSourceLanguage: true }, - required: true, - minRows: 1, - fields: [ - { - type: "row", - fields: [ - { name: fields.pretitle, type: "text" }, - { name: fields.title, type: "text", required: true }, - { name: fields.subtitle, type: "text" }, - ], - }, - { name: fields.summary, type: "textarea" }, - { - type: "tabs", - admin: { - condition: (_, siblingData) => - isDefined(siblingData.language) && isDefined(siblingData.sourceLanguage), + fields: [ + { + type: "row", + fields: [ + slugField({ name: fields.slug, admin: { width: "50%" } }), + imageField({ + name: fields.thumbnail, + relationTo: ContentThumbnails.slug, + admin: { width: "50%" }, + }), + ], + }, + { + type: "row", + fields: [ + { + name: fields.categories, + type: "relationship", + relationTo: [Keys.slug], + filterOptions: { type: { equals: KeysTypes.Categories } }, + hasMany: true, + admin: { allowCreate: false, width: "50%" }, }, - tabs: [ - { - label: "Text", - fields: [ - { - type: "row", - fields: [ - { - name: fields.textTranscribers, - label: "Transcribers", - type: "relationship", - relationTo: Recorders.slug, - hasMany: true, - admin: { - condition: (_, siblingData) => - siblingData.language === siblingData.sourceLanguage, - width: "50%", + { + name: fields.type, + type: "relationship", + relationTo: [Keys.slug], + filterOptions: { type: { equals: KeysTypes.Contents } }, + admin: { allowCreate: false, width: "50%" }, + }, + ], + }, + localizedFields({ + name: fields.translations, + admin: { useAsTitle: fields.title, hasSourceLanguage: true }, + required: true, + minRows: 1, + fields: [ + { + type: "row", + fields: [ + { name: fields.pretitle, type: "text" }, + { name: fields.title, type: "text", required: true }, + { name: fields.subtitle, type: "text" }, + ], + }, + { name: fields.summary, type: "textarea" }, + { + type: "tabs", + admin: { + condition: (_, siblingData) => + isDefined(siblingData.language) && isDefined(siblingData.sourceLanguage), + }, + tabs: [ + { + label: "Text", + fields: [ + { + type: "row", + fields: [ + { + name: fields.textTranscribers, + label: "Transcribers", + type: "relationship", + relationTo: Recorders.slug, + hasMany: true, + admin: { + condition: (_, siblingData) => + siblingData.language === siblingData.sourceLanguage, + width: "50%", + }, }, - }, - { - name: fields.textTranslators, - label: "Translators", - type: "relationship", - relationTo: Recorders.slug, - hasMany: true, - admin: { - condition: (_, siblingData) => - siblingData.language !== siblingData.sourceLanguage, - width: "50%", + { + name: fields.textTranslators, + label: "Translators", + type: "relationship", + relationTo: Recorders.slug, + hasMany: true, + admin: { + condition: (_, siblingData) => + siblingData.language !== siblingData.sourceLanguage, + width: "50%", + }, }, - }, - { - name: fields.textProofreaders, - label: "Proofreaders", - type: "relationship", - relationTo: Recorders.slug, - hasMany: true, - admin: { width: "50%" }, - }, - ], - }, - { - name: fields.textContent, - label: "Content", - labels: { singular: "Block", plural: "Blocks" }, - type: "blocks", - admin: { initCollapsed: true }, - blocks: contentBlocks, - }, - { - name: fields.textNotes, - label: "Notes", - type: "textarea", - }, - ], - }, - { - label: "Video", - fields: [ - { - type: "row", - fields: [ - fileField({ - name: fields.video, - filterOptions: { type: { equals: FileTypes.ContentVideo } }, - admin: { width: "50%" }, - }), - { - name: fields.videoNotes, - label: "Notes", - type: "textarea", - admin: { width: "50%" }, - }, - ], - }, - ], - }, - { - label: "Audio", - fields: [ - { - type: "row", - fields: [ - fileField({ - name: fields.audio, - filterOptions: { type: { equals: FileTypes.ContentAudio } }, - admin: { width: "50%" }, - }), - { - name: fields.audioNotes, - label: "Notes", - type: "textarea", - admin: { width: "50%" }, - }, - ], - }, - ], - }, - ], - }, - ], - }), - ], -}; + { + name: fields.textProofreaders, + label: "Proofreaders", + type: "relationship", + relationTo: Recorders.slug, + hasMany: true, + admin: { width: "50%" }, + }, + ], + }, + { + name: fields.textContent, + label: "Content", + labels: { singular: "Block", plural: "Blocks" }, + type: "blocks", + admin: { initCollapsed: true }, + blocks: contentBlocks, + }, + { + name: fields.textNotes, + label: "Notes", + type: "textarea", + }, + ], + }, + { + label: "Video", + fields: [ + { + type: "row", + fields: [ + fileField({ + name: fields.video, + filterOptions: { type: { equals: FileTypes.ContentVideo } }, + admin: { width: "50%" }, + }), + { + name: fields.videoNotes, + label: "Notes", + type: "textarea", + admin: { width: "50%" }, + }, + ], + }, + ], + }, + { + label: "Audio", + fields: [ + { + type: "row", + fields: [ + fileField({ + name: fields.audio, + filterOptions: { type: { equals: FileTypes.ContentAudio } }, + admin: { width: "50%" }, + }), + { + name: fields.audioNotes, + label: "Notes", + type: "textarea", + admin: { width: "50%" }, + }, + ], + }, + ], + }, + ], + }, + ], + }), + ], + }) +); diff --git a/src/collections/Files/Files.ts b/src/collections/Files/Files.ts index a164561..370b2f2 100644 --- a/src/collections/Files/Files.ts +++ b/src/collections/Files/Files.ts @@ -1,38 +1,34 @@ -import { CollectionConfig } from "payload/types"; import { CollectionGroup, FileTypes } from "../../constants"; -import { collectionSlug } from "../../utils/string"; +import { buildCollectionConfig } from "../../utils/collectionConfig"; const fields = { filename: "filename", type: "type", } as const satisfies Record; -const labels = { - singular: "File", - plural: "Files", -} as const satisfies { singular: string; plural: string }; - -export const Files: CollectionConfig = { - slug: collectionSlug(labels.plural), - labels, - typescript: { interface: labels.singular }, - defaultSort: fields.filename, - admin: { - useAsTitle: fields.filename, - group: CollectionGroup.Media, +export const Files = buildCollectionConfig( + { + singular: "File", + plural: "Files", }, - - fields: [ - { - name: fields.filename, - required: true, - type: "text", + () => ({ + defaultSort: fields.filename, + admin: { + useAsTitle: fields.filename, + group: CollectionGroup.Media, }, - { - name: fields.type, - type: "select", - required: true, - options: Object.entries(FileTypes).map(([value, label]) => ({ label, value })), - }, - ], -}; + fields: [ + { + name: fields.filename, + required: true, + type: "text", + }, + { + name: fields.type, + type: "select", + required: true, + options: Object.entries(FileTypes).map(([value, label]) => ({ label, value })), + }, + ], + }) +); diff --git a/src/collections/Keys/Keys.ts b/src/collections/Keys/Keys.ts index d4f57a8..473f5f6 100644 --- a/src/collections/Keys/Keys.ts +++ b/src/collections/Keys/Keys.ts @@ -2,9 +2,9 @@ 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 { Key } from "../../types/collections"; import { isDefined } from "../../utils/asserts"; +import { buildCollectionConfig } from "../../utils/collectionConfig"; const fields = { slug: "slug", @@ -14,56 +14,54 @@ const fields = { short: "short", } as const satisfies Record; -const labels = { - singular: "Key", - plural: "Keys", -} as const satisfies { singular: string; plural: string }; - const keysTypesWithShort: (keyof typeof KeysTypes)[] = ["Categories", "GamePlatforms"]; -export const Keys: CollectionConfig = { - slug: collectionSlug(labels.plural), - labels, - typescript: { interface: labels.singular }, - defaultSort: fields.slug, - admin: { - useAsTitle: fields.slug, - defaultColumns: [fields.slug, fields.type, fields.translations], - group: CollectionGroup.Meta, +export const Keys: CollectionConfig = buildCollectionConfig( + { + singular: "Key", + plural: "Keys", }, - timestamps: false, - versions: false, - fields: [ - slugField({ name: fields.slug }), - { - name: fields.type, - type: "select", - required: true, - options: Object.entries(KeysTypes).map(([value, label]) => ({ label, value })), + () => ({ + defaultSort: fields.slug, + admin: { + useAsTitle: fields.slug, + defaultColumns: [fields.slug, fields.type, fields.translations], + group: CollectionGroup.Meta, }, - localizedFields({ - name: fields.translations, - interfaceName: "CategoryTranslations", - admin: { - useAsTitle: fields.name, + timestamps: false, + versions: false, + fields: [ + slugField({ name: fields.slug }), + { + name: fields.type, + type: "select", + required: true, + options: Object.entries(KeysTypes).map(([value, label]) => ({ label, value })), }, - 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%", - }, - }, - ], + localizedFields({ + name: fields.translations, + interfaceName: "CategoryTranslations", + admin: { + useAsTitle: fields.name, }, - ], - }), - ], -}; + 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/Languages.ts b/src/collections/Languages.ts index 0d93b69..0238303 100644 --- a/src/collections/Languages.ts +++ b/src/collections/Languages.ts @@ -1,46 +1,43 @@ -import { CollectionConfig } from "payload/types"; import { CollectionGroup } from "../constants"; -import { collectionSlug } from "../utils/string"; +import { buildCollectionConfig } from "../utils/collectionConfig"; const fields = { id: "id", name: "name", } as const satisfies Record; -const labels = { - singular: "Language", - plural: "Languages", -} as const satisfies { singular: string; plural: string }; - -export const Languages: CollectionConfig = { - slug: collectionSlug(labels.plural), - labels, - typescript: { interface: labels.singular }, - defaultSort: fields.name, - admin: { - useAsTitle: fields.name, - defaultColumns: [fields.name, fields.id], - group: CollectionGroup.Meta, +export const Languages = buildCollectionConfig( + { + singular: "Language", + plural: "Languages", }, - timestamps: false, - fields: [ - { - name: fields.id, - type: "text", - unique: true, - required: true, - validate: (value) => { - if (/^[a-z]{2}(-[a-z]{2})?$/g.test(value)) { - return true; - } - return "The code must be a valid IETF language tag and lowercase (i.e: en, pt-pt, fr, zh-tw...)"; + () => ({ + defaultSort: fields.name, + admin: { + useAsTitle: fields.name, + defaultColumns: [fields.name, fields.id], + group: CollectionGroup.Meta, + }, + timestamps: false, + fields: [ + { + name: fields.id, + type: "text", + unique: true, + required: true, + validate: (value) => { + if (/^[a-z]{2}(-[a-z]{2})?$/g.test(value)) { + return true; + } + return "The code must be a valid IETF language tag and lowercase (i.e: en, pt-pt, fr, zh-tw...)"; + }, }, - }, - { - name: fields.name, - type: "text", - unique: true, - required: true, - }, - ], -}; + { + name: fields.name, + type: "text", + unique: true, + required: true, + }, + ], + }) +); diff --git a/src/collections/LibraryItemThumbnails/LibraryItemThumbnails.ts b/src/collections/LibraryItemThumbnails/LibraryItemThumbnails.ts index a1041ee..73a96a3 100644 --- a/src/collections/LibraryItemThumbnails/LibraryItemThumbnails.ts +++ b/src/collections/LibraryItemThumbnails/LibraryItemThumbnails.ts @@ -1,6 +1,5 @@ -import { CollectionConfig } from "payload/types"; import { CollectionGroup } from "../../constants"; -import { collectionSlug } from "../../utils/string"; +import { buildCollectionConfig } from "../../utils/collectionConfig"; const fields = { filename: "filename", @@ -8,57 +7,53 @@ const fields = { 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, +export const LibraryItemThumbnails = buildCollectionConfig( + { + singular: "Library Item Thumbnail", + plural: "Library Item Thumbnails", }, - - 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 }, + ({ labels }) => ({ + 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: "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 }, + { + name: "large", + height: 2048, + width: 2048, + fit: "contain", + formatOptions: { + format: "webp", + options: { effort: 6, quality: 80, alphaQuality: 80 }, + }, }, - }, - ], - }, - - fields: [], -}; + ], + }, + fields: [], + }) +); diff --git a/src/collections/LibraryItems/LibraryItems.ts b/src/collections/LibraryItems/LibraryItems.ts index 117c4d1..f774693 100644 --- a/src/collections/LibraryItems/LibraryItems.ts +++ b/src/collections/LibraryItems/LibraryItems.ts @@ -1,4 +1,3 @@ -import { CollectionConfig } from "payload/types"; import { CollectionGroup, KeysTypes, @@ -8,12 +7,12 @@ import { } 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"; +import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig"; const fields = { status: "status", @@ -42,11 +41,6 @@ const fields = { audioSubtype: "audioSubtype", } as const satisfies Record; -const labels = { - singular: "Library Item", - plural: "Library Items", -} as const satisfies { singular: string; plural: string }; - const validateSizeValue = (value?: number) => { if (isDefined(value) && value <= 0) return "This value must be greater than 0"; return true; @@ -58,216 +52,220 @@ const validateRequiredSizeValue = (value?: number) => { return true; }; -export const LibraryItems: CollectionConfig = { - slug: collectionSlug(labels.plural), - labels, - typescript: { interface: labels.singular }, - defaultSort: fields.slug, - admin: { - useAsTitle: fields.slug, - defaultColumns: [fields.slug, fields.thumbnail, fields.status], - group: CollectionGroup.Collections, - preview: (doc) => `https://accords-library.com/library/${doc.slug}`, +export const LibraryItems = buildVersionedCollectionConfig( + { + singular: "Library Item", + plural: "Library Items", }, - timestamps: true, - versions: { drafts: { autosave: true } }, - fields: [ - { - type: "row", - fields: [ - slugField({ name: fields.slug, admin: { width: "50%" } }), - imageField({ - name: fields.thumbnail, - relationTo: LibraryItemThumbnails.slug, - admin: { width: "50%" }, - }), - ], + () => ({ + defaultSort: fields.slug, + admin: { + useAsTitle: fields.slug, + description: + "A comprehensive list of all Yokoverse’s side materials (books, novellas, artbooks, \ +stage plays, manga, drama CDs, and comics).", + defaultColumns: [fields.slug, fields.thumbnail, fields.status], + group: CollectionGroup.Collections, + preview: (doc) => `https://accords-library.com/library/${doc.slug}`, }, - { - type: "row", - fields: [ - { name: fields.pretitle, type: "text" }, - { name: fields.title, type: "text", required: true }, - { name: fields.subtitle, type: "text" }, - ], - }, - { - type: "row", - fields: [ - { - name: fields.rootItem, - type: "checkbox", - required: true, - defaultValue: true, - admin: { - description: "Only items that can be sold separetely should be root items.", - width: "25%", - }, - }, - { - name: fields.primary, - type: "checkbox", - required: true, - defaultValue: true, - admin: { - description: - "A primary item is an official item that focuses primarly on one or more of our Categories.", - width: "25%", - }, - }, - { - name: fields.digital, - type: "checkbox", - required: true, - defaultValue: false, - admin: { - description: - "The item is the digital version of another item, or the item is sold only digitally.", - width: "25%", - }, - }, - { - name: fields.downloadable, - type: "checkbox", - required: true, - defaultValue: false, - admin: { - description: "Are the scans available for download?", - width: "25%", - }, - }, - ], - }, - { - name: "size", - type: "group", - admin: { condition: (data) => !data.digital }, - fields: [ - { - type: "row", - fields: [ - { - name: fields.width, - type: "number", - validate: validateRequiredSizeValue, - admin: { step: 1, width: "33%", description: "in mm." }, - }, - { - name: fields.height, - type: "number", - validate: validateRequiredSizeValue, - admin: { step: 1, width: "33%", description: "in mm." }, - }, - { - name: fields.thickness, - type: "number", - validate: validateSizeValue, - admin: { step: 1, width: "33%", description: "in mm." }, - }, - ], - }, - ], - }, - { - name: fields.itemType, - type: "radio", - options: Object.entries(LibraryItemsTypes).map(([value, label]) => ({ label, value })), - admin: { - layout: "horizontal", + fields: [ + { + type: "row", + fields: [ + slugField({ name: fields.slug, admin: { width: "50%" } }), + imageField({ + name: fields.thumbnail, + relationTo: LibraryItemThumbnails.slug, + admin: { width: "50%" }, + }), + ], }, - }, - { - name: fields.textual, - type: "group", - admin: { - condition: (data: Partial) => data.itemType === LibraryItemsTypes.Textual, + { + type: "row", + fields: [ + { name: fields.pretitle, type: "text" }, + { name: fields.title, type: "text", required: true }, + { name: fields.subtitle, type: "text" }, + ], }, - 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%" }, + { + type: "row", + fields: [ + { + name: fields.rootItem, + type: "checkbox", + required: true, + defaultValue: true, + admin: { + description: "Only items that can be sold separetely should be root items.", + width: "25%", }, - { - name: fields.textualLanguages, - type: "relationship", - relationTo: [Languages.slug], - hasMany: true, - admin: { allowCreate: false, width: "50%" }, + }, + { + name: fields.primary, + type: "checkbox", + required: true, + defaultValue: true, + admin: { + description: + "A primary item is an official item that focuses primarly on one or more of our Categories.", + width: "25%", }, - ], - }, - { - 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.digital, + type: "checkbox", + required: true, + defaultValue: false, + admin: { + description: + "The item is the digital version of another item, or the item is sold only digitally.", + width: "25%", + }, + }, + { + name: fields.downloadable, + type: "checkbox", + required: true, + defaultValue: false, + admin: { + description: "Are the scans available for download?", + width: "25%", + }, + }, + ], + }, + { + name: "size", + type: "group", + admin: { condition: (data) => !data.digital }, + fields: [ + { + type: "row", + fields: [ + { + name: fields.width, + type: "number", + validate: validateRequiredSizeValue, + admin: { step: 1, width: "33%", description: "in mm." }, }, - }, - { - name: fields.textualPageOrder, - label: "Page Order", - type: "radio", - options: Object.entries(LibraryItemsTextualPageOrders).map(([value, label]) => ({ - label, - value, - })), - admin: { - layout: "horizontal", - width: "33%", + { + name: fields.height, + type: "number", + validate: validateRequiredSizeValue, + admin: { step: 1, width: "33%", description: "in mm." }, }, - }, - ], - }, - ], - }, - { - name: fields.audio, - type: "group", - admin: { - condition: (data: Partial) => data.itemType === LibraryItemsTypes.Audio, + { + name: fields.thickness, + type: "number", + validate: validateSizeValue, + admin: { step: 1, width: "33%", description: "in mm." }, + }, + ], + }, + ], }, - 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.itemType, + type: "radio", + options: Object.entries(LibraryItemsTypes).map(([value, label]) => ({ label, value })), + admin: { + layout: "horizontal", }, - ], - }, - { - name: fields.releaseDate, - type: "date", - admin: { - date: { pickerAppearance: "dayOnly", displayFormat: "yyyy-MM-dd" }, - position: "sidebar", }, - }, - ], -}; + { + 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/PostThumbnails/PostThumbnails.ts b/src/collections/PostThumbnails/PostThumbnails.ts index 2f7b58d..45fd3a1 100644 --- a/src/collections/PostThumbnails/PostThumbnails.ts +++ b/src/collections/PostThumbnails/PostThumbnails.ts @@ -1,6 +1,5 @@ -import { CollectionConfig } from "payload/types"; import { CollectionGroup } from "../../constants"; -import { collectionSlug } from "../../utils/string"; +import { buildCollectionConfig } from "../../utils/collectionConfig"; const fields = { filename: "filename", @@ -8,45 +7,41 @@ const fields = { filesize: "filesize", } as const satisfies Record; -const labels = { - singular: "Post Thumbnail", - plural: "Post Thumbnails", -} as const satisfies { singular: string; plural: string }; - -export const PostThumbnails: CollectionConfig = { - slug: collectionSlug(labels.plural), - labels, - typescript: { interface: labels.singular }, - defaultSort: fields.filename, - admin: { - useAsTitle: fields.filename, - group: CollectionGroup.Media, +export const PostThumbnails = buildCollectionConfig( + { + singular: "Post Thumbnail", + plural: "Post Thumbnails", }, - - 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 }, + ({ labels }) => ({ + 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 }, + { + name: "medium", + height: 1000, + width: 1500, + formatOptions: { + format: "webp", + options: { effort: 6, quality: 80, alphaQuality: 80 }, + }, }, - }, - ], - }, - - fields: [], -}; + ], + }, + fields: [], + }) +); diff --git a/src/collections/Posts/Posts.ts b/src/collections/Posts/Posts.ts index a79d20e..b691750 100644 --- a/src/collections/Posts/Posts.ts +++ b/src/collections/Posts/Posts.ts @@ -1,4 +1,3 @@ -import { CollectionConfig } from "payload/types"; import { slugField } from "../../fields/slugField/slugField"; import { imageField } from "../../fields/imageField/imageField"; import { CollectionGroup, KeysTypes } from "../../constants"; @@ -7,8 +6,8 @@ import { localizedFields } from "../../fields/translatedFields/translatedFields" import { isDefined, isUndefined } from "../../utils/asserts"; import { removeTranslatorsForTranscripts } from "./hooks/beforeValidate"; import { Keys } from "../Keys/Keys"; -import { collectionSlug } from "../../utils/string"; import { PostThumbnails } from "../PostThumbnails/PostThumbnails"; +import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig"; const fields = { slug: "slug", @@ -26,133 +25,135 @@ const fields = { proofreaders: "proofreaders", } as const satisfies Record; -const labels = { - singular: "Post", - plural: "Posts", -} as const satisfies { singular: string; plural: string }; - -export const Posts: CollectionConfig = { - slug: collectionSlug(labels.plural), - labels, - typescript: { interface: labels.singular }, - defaultSort: fields.slug, - admin: { - useAsTitle: fields.slug, - defaultColumns: [fields.slug, fields.thumbnail, fields.categories], - group: CollectionGroup.Collections, - preview: (doc) => `https://accords-library.com/news/${doc.slug}`, +export const Posts = buildVersionedCollectionConfig( + { + singular: "Post", + plural: "Posts", }, - hooks: { - beforeValidate: [removeTranslatorsForTranscripts], - }, - timestamps: true, - versions: { drafts: { autosave: true } }, - fields: [ - { - type: "row", - fields: [ - slugField({ name: fields.slug, admin: { width: "50%" } }), - imageField({ - name: fields.thumbnail, - relationTo: PostThumbnails.slug, - admin: { width: "50%" }, - }), - ], + () => ({ + defaultSort: fields.slug, + admin: { + useAsTitle: fields.slug, + description: + "News articles written by our Recorders! Here you will find announcements about \ +new merch/items releases, guides, theories, unboxings, showcases...", + defaultColumns: [fields.slug, fields.thumbnail, fields.categories], + group: CollectionGroup.Collections, + preview: (doc) => `https://accords-library.com/news/${doc.slug}`, }, - { - type: "row", - fields: [ - { - name: fields.authors, - type: "relationship", - relationTo: [Recorders.slug], - required: true, - minRows: 1, - hasMany: true, - admin: { width: "35%" }, - }, - { - name: fields.categories, - type: "relationship", - relationTo: [Keys.slug], - filterOptions: { type: { equals: KeysTypes.Categories } }, - hasMany: true, - admin: { allowCreate: false, width: "35%" }, - }, - ], + hooks: { + beforeValidate: [removeTranslatorsForTranscripts], }, - localizedFields({ - name: fields.translations, - admin: { useAsTitle: fields.title, hasSourceLanguage: true }, - required: true, - minRows: 1, - fields: [ - { name: fields.title, type: "text", required: true }, - { name: fields.summary, type: "textarea" }, - { - type: "row", - fields: [ - { - name: fields.translators, - type: "relationship", - relationTo: Recorders.slug, - hasMany: true, - admin: { - condition: (_, siblingData) => { + fields: [ + { + type: "row", + fields: [ + slugField({ name: fields.slug, admin: { width: "50%" } }), + imageField({ + name: fields.thumbnail, + relationTo: PostThumbnails.slug, + admin: { width: "50%" }, + }), + ], + }, + { + type: "row", + fields: [ + { + name: fields.authors, + type: "relationship", + relationTo: [Recorders.slug], + required: true, + minRows: 1, + hasMany: true, + admin: { width: "35%" }, + }, + { + name: fields.categories, + type: "relationship", + relationTo: [Keys.slug], + filterOptions: { type: { equals: KeysTypes.Categories } }, + hasMany: true, + admin: { allowCreate: false, width: "35%" }, + }, + ], + }, + localizedFields({ + name: fields.translations, + admin: { useAsTitle: fields.title, hasSourceLanguage: true }, + required: true, + minRows: 1, + fields: [ + { name: fields.title, type: "text", required: true }, + { name: fields.summary, type: "textarea" }, + { + type: "row", + fields: [ + { + name: fields.translators, + type: "relationship", + relationTo: Recorders.slug, + hasMany: true, + admin: { + condition: (_, siblingData) => { + if ( + isUndefined(siblingData.language) || + isUndefined(siblingData.sourceLanguage) + ) { + return false; + } + return siblingData.language !== siblingData.sourceLanguage; + }, + width: "50%", + }, + validate: (translators, { siblingData }) => { if ( isUndefined(siblingData.language) || isUndefined(siblingData.sourceLanguage) ) { - return false; + return true; } - return siblingData.language !== siblingData.sourceLanguage; + if (siblingData.language === siblingData.sourceLanguage) { + return true; + } + if (isDefined(translators) && translators.length > 0) { + return true; + } + return "This field is required when the language is different from the source language."; }, - width: "50%", }, - validate: (translators, { siblingData }) => { - if (isUndefined(siblingData.language) || isUndefined(siblingData.sourceLanguage)) { - return true; - } - if (siblingData.language === siblingData.sourceLanguage) { - return true; - } - if (isDefined(translators) && translators.length > 0) { - return true; - } - return "This field is required when the language is different from the source language."; + { + name: fields.proofreaders, + type: "relationship", + relationTo: Recorders.slug, + hasMany: true, + admin: { width: "50%" }, }, - }, - { - name: fields.proofreaders, - type: "relationship", - relationTo: Recorders.slug, - hasMany: true, - admin: { width: "50%" }, - }, - ], + ], + }, + { name: fields.content, type: "richText", admin: { hideGutter: true } }, + ], + }), + { + name: fields.publishedDate, + type: "date", + defaultValue: new Date().toISOString(), + admin: { + date: { pickerAppearance: "dayOnly", displayFormat: "yyyy-MM-dd" }, + position: "sidebar", }, - { name: fields.content, type: "richText", admin: { hideGutter: true } }, - ], - }), - { - name: fields.publishedDate, - type: "date", - defaultValue: new Date().toISOString(), - admin: { - date: { pickerAppearance: "dayOnly", displayFormat: "yyyy-MM-dd" }, - position: "sidebar", + required: true, }, - 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", + { + 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/Posts/hooks/beforeValidate.ts b/src/collections/Posts/hooks/beforeValidate.ts index 817a205..9a6d1d9 100644 --- a/src/collections/Posts/hooks/beforeValidate.ts +++ b/src/collections/Posts/hooks/beforeValidate.ts @@ -5,7 +5,7 @@ export const removeTranslatorsForTranscripts: CollectionBeforeValidateHook data: { translations, ...data }, }) => ({ ...data, - translations: translations.map(({ translators, ...translation }) => { + translations: translations?.map(({ translators, ...translation }) => { if (translation.language === translation.sourceLanguage) { return { ...translation, translators: [] }; } diff --git a/src/collections/RecorderThumbnails/RecorderThumbnails.ts b/src/collections/RecorderThumbnails/RecorderThumbnails.ts index 74449eb..3cc3bff 100644 --- a/src/collections/RecorderThumbnails/RecorderThumbnails.ts +++ b/src/collections/RecorderThumbnails/RecorderThumbnails.ts @@ -1,6 +1,5 @@ -import { CollectionConfig } from "payload/types"; import { CollectionGroup } from "../../constants"; -import { collectionSlug } from "../../utils/string"; +import { buildCollectionConfig } from "../../utils/collectionConfig"; const fields = { filename: "filename", @@ -8,46 +7,42 @@ const fields = { 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, +export const RecorderThumbnails = buildCollectionConfig( + { + singular: "Recorder Thumbnail", + plural: "Recorder Thumbnails", }, - - 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 }, + ({ labels }) => ({ + 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 }, + { + name: "small", + height: 128, + width: 128, + formatOptions: { + format: "webp", + options: { effort: 6, quality: 80, alphaQuality: 80 }, + }, }, - }, - ], - }, - - fields: [], -}; + ], + }, + fields: [], + }) +); diff --git a/src/collections/Recorders/Recorders.ts b/src/collections/Recorders/Recorders.ts index 0102a32..7c2936a 100644 --- a/src/collections/Recorders/Recorders.ts +++ b/src/collections/Recorders/Recorders.ts @@ -1,11 +1,10 @@ -import { CollectionConfig } from "payload/types"; import { localizedFields } from "../../fields/translatedFields/translatedFields"; import { Languages } from "../Languages"; import { beforeDuplicate } from "./hooks/beforeDuplicate"; import { CollectionGroup } from "../../constants"; -import { collectionSlug } from "../../utils/string"; import { RecorderThumbnails } from "../RecorderThumbnails/RecorderThumbnails"; import { imageField } from "../../fields/imageField/imageField"; +import { buildCollectionConfig } from "../../utils/collectionConfig"; const fields = { username: "username", @@ -16,79 +15,77 @@ const fields = { avatar: "avatar", } as const satisfies Record; -const labels = { - singular: "Recorder", - plural: "Recorders", -} as const satisfies { singular: string; plural: string }; - -export const Recorders: CollectionConfig = { - slug: collectionSlug(labels.plural), - labels, - typescript: { interface: labels.singular }, - defaultSort: fields.username, - admin: { - useAsTitle: fields.username, - 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.avatar, - fields.anonymize, - fields.biographies, - fields.languages, - ], - group: CollectionGroup.Meta, +export const Recorders = buildCollectionConfig( + { + singular: "Recorder", + plural: "Recorders", }, - timestamps: false, - fields: [ - { - type: "row", - fields: [ - { - name: fields.username, - type: "text", - unique: true, - required: true, - admin: { description: "The username must be unique", width: "33%" }, - }, - imageField({ - name: fields.avatar, - relationTo: RecorderThumbnails.slug, - admin: { width: "66%" }, - }), + () => ({ + defaultSort: fields.username, + admin: { + useAsTitle: fields.username, + 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.avatar, + fields.anonymize, + fields.biographies, + fields.languages, ], + group: CollectionGroup.Meta, }, - { - name: fields.languages, - type: "relationship", - relationTo: Languages.slug, - hasMany: true, - admin: { - allowCreate: false, - description: "List of language(s) that this recorder is familiar with", + timestamps: false, + fields: [ + { + type: "row", + fields: [ + { + name: fields.username, + type: "text", + unique: true, + required: true, + admin: { description: "The username must be unique", width: "33%" }, + }, + imageField({ + name: fields.avatar, + relationTo: RecorderThumbnails.slug, + admin: { width: "66%" }, + }), + ], }, - }, - localizedFields({ - name: fields.biographies, - interfaceName: "RecorderBiographies", - admin: { - useAsTitle: fields.biography, - description: - "A short personal description about you or your involvement with this project or the franchise", + { + name: fields.languages, + type: "relationship", + relationTo: Languages.slug, + hasMany: true, + admin: { + allowCreate: false, + description: "List of language(s) that this recorder is familiar with", + }, }, - 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", + localizedFields({ + name: fields.biographies, + interfaceName: "RecorderBiographies", + admin: { + useAsTitle: fields.biography, + description: + "A short personal description about you or your involvement with this project or the franchise", + }, + 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/collections/Users.ts b/src/collections/Users.ts index 87f68b6..a956ef5 100644 --- a/src/collections/Users.ts +++ b/src/collections/Users.ts @@ -1,27 +1,60 @@ -import { CollectionConfig } from "payload/types"; -import { CollectionGroup } from "../constants"; -import { collectionSlug } from "../utils/string"; +import { CollectionGroup, UserRoles } from "../constants"; +import { Recorders } from "./Recorders/Recorders"; +import { buildCollectionConfig } from "../utils/collectionConfig"; const fields = { + recorder: "recorder", + name: "name", email: "email", + role: "role", } as const satisfies Record; -const labels = { - singular: "User", - plural: "Users", -} as const satisfies { singular: string; plural: string }; - -export const Users: CollectionConfig = { - slug: collectionSlug(labels.plural), - auth: true, - labels, - typescript: { interface: labels.singular }, - defaultSort: fields.email, - admin: { - useAsTitle: fields.email, - defaultColumns: [fields.email], - group: CollectionGroup.Administration, +export const Users = buildCollectionConfig( + { + singular: "User", + plural: "Users", }, - timestamps: false, - fields: [], -}; + () => ({ + auth: true, + defaultSort: fields.recorder, + admin: { + useAsTitle: fields.name, + defaultColumns: [fields.recorder, fields.name, fields.email, fields.role], + group: CollectionGroup.Administration, + }, + timestamps: false, + fields: [ + { + type: "row", + fields: [ + { + name: fields.recorder, + type: "relationship", + relationTo: Recorders.slug, + required: true, + admin: { width: "33%" }, + }, + { + name: fields.name, + type: "text", + required: true, + unique: true, + admin: { width: "33%" }, + }, + { + name: fields.role, + required: true, + defaultValue: [UserRoles.Recorder], + type: "select", + hasMany: true, + options: Object.entries(UserRoles).map(([value, label]) => ({ + label, + value, + })), + admin: { width: "33%" }, + }, + ], + }, + ], + }) +); diff --git a/src/constants.ts b/src/constants.ts index b475ac5..2aa33f6 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -42,3 +42,8 @@ export enum LibraryItemsTextualPageOrders { LeftToRight = "Left to right", RightToLeft = "Right to left", } + +export enum UserRoles { + Admin = "Admin", + Recorder = "Recorder", +} diff --git a/src/fields/imageField/Cell.tsx b/src/fields/imageField/Cell.tsx index 75f0f7f..8db28f2 100644 --- a/src/fields/imageField/Cell.tsx +++ b/src/fields/imageField/Cell.tsx @@ -1,11 +1,13 @@ import { Props } from "payload/components/views/Cell"; import { useState, useEffect } from "react"; import React from "react"; +import { isUndefined } from "../../utils/asserts"; export const Cell = ({ cellData, field }: Props): JSX.Element => { const [imageURL, setImageURL] = useState(); useEffect(() => { const fetchUrl = async () => { + if (isUndefined(cellData)) return; if (typeof cellData !== "string") return; if (field.type !== "upload") return; const result = await (await fetch(`/api/${field.relationTo}/${cellData}`)).json(); diff --git a/src/fields/translatedFields/translatedFields.ts b/src/fields/translatedFields/translatedFields.ts index af34306..3ce1073 100644 --- a/src/fields/translatedFields/translatedFields.ts +++ b/src/fields/translatedFields/translatedFields.ts @@ -44,10 +44,11 @@ export const localizedFields = ({ components: { Cell: ({ cellData }) => Cell({ - cellData: cellData.map((row) => ({ - language: row.language, - title: isDefined(useAsTitle) ? row[useAsTitle] : undefined, - })), + cellData: + cellData?.map((row) => ({ + language: row.language, + title: isDefined(useAsTitle) ? row[useAsTitle] : undefined, + })) ?? [], }), RowLabel: ({ data }) => RowLabel({ diff --git a/src/types/collections.ts b/src/types/collections.ts index da5a1b9..df04ee8 100644 --- a/src/types/collections.ts +++ b/src/types/collections.ts @@ -93,6 +93,7 @@ export interface LibraryItem { }[]; }; releaseDate?: string; + lastModifiedBy: string | User; updatedAt: string; createdAt: string; _status?: "draft" | "published"; @@ -154,6 +155,57 @@ export interface Language { id: string; name: string; } +export interface User { + id: string; + recorder: string | Recorder; + name: string; + role: ("Admin" | "Recorder")[]; + email: string; + resetPasswordToken?: string; + resetPasswordExpiration?: string; + salt?: string; + hash?: string; + loginAttempts?: number; + lockUntil?: string; + password?: string; +} +export interface Recorder { + id: string; + username: string; + avatar?: string | RecorderThumbnail; + languages?: string[] | Language[]; + biographies?: RecorderBiographies; + anonymize: boolean; +} +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 Content { id: string; slug: string; @@ -188,6 +240,7 @@ export interface Content { audio?: string | File; id?: string; }[]; + lastModifiedBy: string | User; updatedAt: string; createdAt: string; _status?: "draft" | "published"; @@ -221,43 +274,6 @@ export interface ContentThumbnail { }; }; } -export interface Recorder { - id: string; - username: string; - avatar?: string | RecorderThumbnail; - languages?: string[] | Language[]; - biographies?: RecorderBiographies; - anonymize: boolean; -} -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: { [k: string]: unknown; @@ -511,6 +527,7 @@ export interface Post { }[]; publishedDate: string; hidden?: boolean; + lastModifiedBy: string | User; updatedAt: string; createdAt: string; _status?: "draft" | "published"; @@ -544,14 +561,3 @@ export interface PostThumbnail { }; }; } -export interface User { - id: string; - email: string; - resetPasswordToken?: string; - resetPasswordExpiration?: string; - salt?: string; - hash?: string; - loginAttempts?: number; - lockUntil?: string; - password?: string; -} diff --git a/src/utils/collectionConfig.ts b/src/utils/collectionConfig.ts new file mode 100644 index 0000000..6392a11 --- /dev/null +++ b/src/utils/collectionConfig.ts @@ -0,0 +1,22 @@ +import { CollectionConfig } from "payload/types"; +import slugify from "slugify"; + +export type BuildCollectionConfig = Omit; + +export type GenerationFunctionProps = { + labels: { singular: string; plural: string }; + slug: string; +}; + +export const buildCollectionConfig = ( + labels: { singular: string; plural: string }, + generationFunction: (props: GenerationFunctionProps) => BuildCollectionConfig +): CollectionConfig => { + const slug = slugify(labels.plural, { lower: true, strict: true, trim: true }); + const config = generationFunction({ labels, slug }); + return { + ...config, + slug, + typescript: { interface: labels.singular }, + }; +}; diff --git a/src/utils/string.ts b/src/utils/string.ts index 74912fc..c684cd3 100644 --- a/src/utils/string.ts +++ b/src/utils/string.ts @@ -1,11 +1,7 @@ import ISO6391 from "iso-639-1"; -import slugify from "slugify"; export const shortenEllipsis = (text: string, length: number): string => text.length - 3 > length ? `${text.substring(0, length)}...` : text; export const formatLanguageCode = (code: string): string => - ISO6391.validate(code) ? ISO6391.getName(code) : code; - -export const collectionSlug = (text: string): string => - slugify(text, { lower: true, strict: true, trim: true }); + ISO6391.validate(code) ? ISO6391.getName(code) : code; \ No newline at end of file diff --git a/src/utils/versionedCollectionConfig.ts b/src/utils/versionedCollectionConfig.ts new file mode 100644 index 0000000..b125a15 --- /dev/null +++ b/src/utils/versionedCollectionConfig.ts @@ -0,0 +1,52 @@ +import { CollectionBeforeChangeHook, CollectionConfig, RelationshipField } from "payload/types"; +import { Users } from "../collections/Users"; +import { + BuildCollectionConfig, + GenerationFunctionProps, + buildCollectionConfig, +} from "./collectionConfig"; + +const fields = { lastModifiedBy: "lastModifiedBy" }; + +const beforeChangeLastModifiedBy: CollectionBeforeChangeHook = async ({ + data: { updatedBy, ...data }, + req, +}) => { + console.log(data, req.user); + return { + ...data, + [fields.lastModifiedBy]: req.user.id, + }; +}; + +const lastModifiedByField = (): RelationshipField => ({ + name: fields.lastModifiedBy, + type: "relationship", + required: true, + relationTo: Users.slug, + admin: { readOnly: true, position: "sidebar" }, +}); + +type BuildVersionedCollectionConfig = Omit; + +export const buildVersionedCollectionConfig = ( + labels: { singular: string; plural: string }, + generationFunction: (props: GenerationFunctionProps) => BuildVersionedCollectionConfig +): CollectionConfig => { + const { + hooks: { beforeChange, ...otherHooks } = {}, + fields, + ...otherParams + } = buildCollectionConfig(labels, generationFunction); + + return { + ...otherParams, + timestamps: true, + versions: { drafts: { autosave: { interval: 2000 } } }, + hooks: { + ...otherHooks, + beforeChange: [...(beforeChange ?? []), beforeChangeLastModifiedBy], + }, + fields: [...fields, lastModifiedByField()], + }; +};