diff --git a/package-lock.json b/package-lock.json index b02772e..3b6c919 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,15 +12,10 @@ "@fontsource/vollkorn": "^5.0.8", "clean-deep": "^3.4.0", "cross-env": "^7.0.3", - "dotenv": "^16.3.1", - "express": "^4.18.2", "language-tags": "^1.0.8", "luxon": "^3.4.0", "payload": "^1.13.3", - "qs": "^6.11.2", - "slugify": "^1.6.6", - "styled-components": "^6.0.7", - "unset-value": "^2.0.1" + "styled-components": "^6.0.7" }, "devDependencies": { "@types/dotenv": "^8.2.0", @@ -6887,6 +6882,7 @@ "version": "16.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "dev": true, "engines": { "node": ">=12" }, @@ -7565,17 +7561,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/get-value": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-3.0.1.tgz", - "integrity": "sha512-mKZj9JLQrwMBtj5wxi6MH8Z5eSKaERpAwjg43dPtlGI1ZVEgH/qC7T8/6R2OBSUA+zzHBZgICsVJaEIV2tKTDA==", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=6.0" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -7817,29 +7802,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-value": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-2.0.2.tgz", - "integrity": "sha512-ybKOlcRsK2MqrM3Hmz/lQxXHZ6ejzSPzpNabKB45jb5qDgJvKPa3SdapTsTLwEb9WltgWpOmNax7i+DzNOk4TA==", - "dependencies": { - "get-value": "^3.0.0", - "has-values": "^2.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/has-values": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-2.0.1.tgz", - "integrity": "sha512-+QdH3jOmq9P8GfdjFg0eJudqx1FqU62NQJ4P16rOEHeRdl7ckgwn6uqQjzYE0ZoHVV/e5E2esuJ5Gl5+HUW19w==", - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -12644,14 +12606,6 @@ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.6.tgz", "integrity": "sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA==" }, - "node_modules/slugify": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", - "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -13540,26 +13494,6 @@ "node": ">= 0.8" } }, - "node_modules/unset-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-2.0.1.tgz", - "integrity": "sha512-2hvrBfjUE00PkqN+q0XP6yRAOGrR06uSiUoIQGZkc7GxvQ9H7v8quUPNtZjMg4uux69i8HWpIjLPUKwCuRGyNg==", - "dependencies": { - "has-value": "^2.0.2", - "isobject": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/unset-value/node_modules/isobject": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz", - "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", diff --git a/package.json b/package.json index 488ae25..2a21db7 100644 --- a/package.json +++ b/package.json @@ -24,15 +24,10 @@ "@fontsource/vollkorn": "^5.0.8", "clean-deep": "^3.4.0", "cross-env": "^7.0.3", - "dotenv": "^16.3.1", - "express": "^4.18.2", "language-tags": "^1.0.8", "luxon": "^3.4.0", "payload": "^1.13.3", - "qs": "^6.11.2", - "slugify": "^1.6.6", - "styled-components": "^6.0.7", - "unset-value": "^2.0.1" + "styled-components": "^6.0.7" }, "devDependencies": { "@types/dotenv": "^8.2.0", diff --git a/src/collections/ChronologyEras/ChronologyEras.ts b/src/collections/ChronologyEras/ChronologyEras.ts index ce2e078..ea02a91 100644 --- a/src/collections/ChronologyEras/ChronologyEras.ts +++ b/src/collections/ChronologyEras/ChronologyEras.ts @@ -1,10 +1,13 @@ import { CollectionConfig } from "payload/types"; import { mustBeAdmin } from "../../accesses/mustBeAdmin"; import { CollectionGroups, Collections } from "../../constants"; +import { backPropagationField } from "../../fields/backPropagationField/backPropagationField"; import { slugField } from "../../fields/slugField/slugField"; -import { localizedFields } from "../../fields/translatedFields/translatedFields"; +import { translatedFields } from "../../fields/translatedFields/translatedFields"; import { buildCollectionConfig } from "../../utils/collectionConfig"; import { importFromStrapi } from "./endpoints/importFromStrapi"; +import { beforeValidateEndingGreaterThanStarting } from "./hooks/beforeValidateEndingGreaterThanStarting"; +import { beforeValidateNoIntersection } from "./hooks/beforeValidateNoIntersection"; const fields = { slug: "slug", @@ -13,58 +16,71 @@ const fields = { translations: "translations", translationsTitle: "title", translationsDescription: "description", + events: "events", } as const satisfies Record; -export const ChronologyEras: CollectionConfig = buildCollectionConfig( - Collections.ChronologyEras, - { +export const ChronologyEras: CollectionConfig = buildCollectionConfig({ + slug: Collections.ChronologyEras, + labels: { singular: "Chronology Era", plural: "Chronology Eras", }, - () => ({ - defaultSort: fields.startingYear, - admin: { - group: CollectionGroups.Collections, - defaultColumns: [fields.slug, fields.startingYear, fields.endingYear, fields.translations], - useAsTitle: fields.slug, + defaultSort: fields.startingYear, + admin: { + group: CollectionGroups.Collections, + defaultColumns: [fields.slug, fields.startingYear, fields.endingYear, fields.translations], + useAsTitle: fields.slug, + }, + access: { + create: mustBeAdmin, + delete: mustBeAdmin, + }, + hooks: { + beforeValidate: [beforeValidateEndingGreaterThanStarting, beforeValidateNoIntersection], + }, + endpoints: [importFromStrapi], + fields: [ + slugField({ name: fields.slug }), + { + type: "row", + fields: [ + { + name: fields.startingYear, + type: "number", + min: 0, + required: true, + admin: { width: "50%", description: "The year the era started (year included)" }, + }, + { + name: fields.endingYear, + type: "number", + min: 0, + required: true, + admin: { width: "50%", description: "The year the era ended (year included)" }, + }, + ], }, - access: { - create: mustBeAdmin, - delete: mustBeAdmin, - }, - endpoints: [importFromStrapi], - fields: [ - slugField({ name: fields.slug }), - { - type: "row", - fields: [ - { - name: fields.startingYear, - type: "number", - min: 0, - required: true, - admin: { width: "50%", description: "The year the era started (year included)" }, - }, - { - name: fields.endingYear, - type: "number", - min: 0, - required: true, - admin: { width: "50%", description: "The year the era ended (year included)" }, - }, - ], - }, - localizedFields({ - name: fields.translations, - admin: { useAsTitle: fields.translationsTitle }, - fields: [ - { name: fields.translationsTitle, type: "text", required: true }, - { - name: fields.translationsDescription, - type: "textarea", - }, + translatedFields({ + name: fields.translations, + admin: { useAsTitle: fields.translationsTitle }, + fields: [ + { name: fields.translationsTitle, type: "text", required: true }, + { + name: fields.translationsDescription, + type: "textarea", + }, + ], + }), + backPropagationField({ + name: fields.events, + hasMany: true, + relationTo: Collections.ChronologyItems, + where: ({ startingYear, endingYear }) => ({ + and: [ + { "date.year": { greater_than_equal: startingYear } }, + { "date.year": { less_than_equal: endingYear } }, ], }), - ], - }) -); + }), + ], +}); diff --git a/src/collections/ChronologyEras/hooks/beforeValidateEndingGreaterThanStarting.ts b/src/collections/ChronologyEras/hooks/beforeValidateEndingGreaterThanStarting.ts new file mode 100644 index 0000000..645f1db --- /dev/null +++ b/src/collections/ChronologyEras/hooks/beforeValidateEndingGreaterThanStarting.ts @@ -0,0 +1,10 @@ +import { CollectionBeforeValidateHook } from "payload/types"; +import { ChronologyEra } from "../../../types/collections"; + +export const beforeValidateEndingGreaterThanStarting: CollectionBeforeValidateHook< + ChronologyEra +> = async ({ data: { startingYear, endingYear } }) => { + if (endingYear < startingYear) { + throw new Error("The ending year cannot be before the starting year."); + } +}; diff --git a/src/collections/ChronologyEras/hooks/beforeValidateNoIntersection.ts b/src/collections/ChronologyEras/hooks/beforeValidateNoIntersection.ts new file mode 100644 index 0000000..09917d1 --- /dev/null +++ b/src/collections/ChronologyEras/hooks/beforeValidateNoIntersection.ts @@ -0,0 +1,23 @@ +import payload from "payload"; +import { CollectionBeforeValidateHook } from "payload/types"; +import { Collections } from "../../../constants"; +import { ChronologyEra } from "../../../types/collections"; +import { hasIntersection } from "../../../utils/asserts"; + +export const beforeValidateNoIntersection: CollectionBeforeValidateHook = async ({ + data: { startingYear, endingYear }, +}) => { + const otherEras = await payload.find({ + collection: Collections.ChronologyEras, + limit: 100, + }); + + otherEras.docs.forEach((otherEra: ChronologyEra) => { + if (hasIntersection([startingYear, endingYear], [otherEra.startingYear, otherEra.endingYear])) { + throw new Error( + `This era (${startingYear} -> ${endingYear}) is intersecting with the era\ + "${otherEra.slug}" (${otherEra.startingYear} -> ${otherEra.endingYear})` + ); + } + }); +}; diff --git a/src/collections/ChronologyItems/ChronologyItems.ts b/src/collections/ChronologyItems/ChronologyItems.ts index 44a518f..d9f2cd6 100644 --- a/src/collections/ChronologyItems/ChronologyItems.ts +++ b/src/collections/ChronologyItems/ChronologyItems.ts @@ -6,115 +6,137 @@ import { publishStatusFilters, } from "../../components/QuickFilters"; import { CollectionGroups, Collections } from "../../constants"; -import { localizedFields } from "../../fields/translatedFields/translatedFields"; -import { isDefined, isUndefined } from "../../utils/asserts"; +import { translatedFields } from "../../fields/translatedFields/translatedFields"; +import { isEmpty, isUndefined } from "../../utils/asserts"; import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig"; import { importFromStrapi } from "./endpoints/importFromStrapi"; +import { beforeValidatePopulateNameField } from "./hooks/beforeValidatePopulateNameField"; const fields = { name: "name", events: "events", + eventsSource: "source", eventsTranslations: "translations", eventsTranslationsTitle: "title", eventsTranslationsDescription: "description", eventsTranslationsNotes: "notes", date: "date", - year: "year", - month: "month", - day: "day", + dateYear: "year", + dateMonth: "month", + dateDay: "day", + era: "era", status: "_status", } as const satisfies Record; -export const ChronologyItems: CollectionConfig = buildVersionedCollectionConfig( - Collections.ChronologyItems, - { +export const ChronologyItems: CollectionConfig = buildVersionedCollectionConfig({ + slug: Collections.ChronologyItems, + labels: { singular: "Chronology Item", plural: "Chronology Items", }, - () => ({ - defaultSort: fields.name, - admin: { - group: CollectionGroups.Collections, - defaultColumns: [fields.name, fields.events, fields.status], - useAsTitle: fields.name, - components: { - BeforeListTable: [ - () => - QuickFilters({ - slug: Collections.ChronologyItems, - filterGroups: [ - languageBasedFilters("events.translations.language"), - publishStatusFilters, - ], - }), - ], - }, - }, - endpoints: [importFromStrapi], - fields: [ - { - name: fields.name, - type: "text", - admin: { hidden: true }, - hooks: { - beforeValidate: [ - ({ - data: { - date: { year, month, day }, - }, - }) => - [ - String(year ?? "?????").padStart(5, "0"), - String(month ?? "??").padStart(2, "0"), - String(day ?? "??").padStart(2, "0"), - ].join("-"), - ], - }, - }, - { - type: "group", - name: fields.date, - validate: ({ year, month, day } = {}) => { - if (isDefined(day)) { - if (isUndefined(month)) return "A month is required if a day is set"; - const stringDate = `${year}/${month}/${day}`; - if (!DateTime.fromObject({ year, month, day }).isValid) - return `The given date (${stringDate}) is not a valid date.`; - } - return true; - }, - fields: [ - { - type: "row", - fields: [ - { - name: fields.year, - type: "number", - required: true, - min: 0, - admin: { width: "33%" }, - }, - { name: fields.month, type: "number", min: 1, max: 12, admin: { width: "33%" } }, - { name: fields.day, type: "number", min: 1, max: 31, admin: { width: "33%" } }, - ], - }, - ], - }, - { - name: fields.events, - type: "array", - fields: [ - localizedFields({ - name: fields.eventsTranslations, - admin: { useAsTitle: fields.eventsTranslationsTitle }, - fields: [ - { name: fields.eventsTranslationsTitle, type: "text" }, - { name: fields.eventsTranslationsDescription, type: "textarea" }, - { name: fields.eventsTranslationsNotes, type: "textarea" }, + defaultSort: fields.name, + admin: { + group: CollectionGroups.Collections, + defaultColumns: [fields.name, fields.events, fields.status], + useAsTitle: fields.name, + components: { + BeforeListTable: [ + () => + QuickFilters({ + slug: Collections.ChronologyItems, + filterGroups: [ + languageBasedFilters("events.translations.language"), + publishStatusFilters, ], }), - ], + ], + }, + }, + endpoints: [importFromStrapi], + fields: [ + { + name: fields.name, + type: "text", + admin: { hidden: true }, + hooks: { + beforeValidate: [beforeValidatePopulateNameField], }, - ], - }) -); + }, + { + type: "group", + name: fields.date, + validate: ({ year, month, day } = {}) => { + if (isUndefined(day)) return true; + if (isUndefined(month)) return "A month is required if a day is set"; + const stringDate = `${year}/${month}/${day}`; + if (!DateTime.fromObject({ year, month, day }).isValid) { + return `The given date (${stringDate}) is not a valid date.`; + } + }, + fields: [ + { + type: "row", + fields: [ + { + name: fields.dateYear, + type: "number", + required: true, + min: 0, + admin: { width: "33%" }, + }, + { name: fields.dateMonth, type: "number", min: 1, max: 12, admin: { width: "33%" } }, + { name: fields.dateDay, type: "number", min: 1, max: 31, admin: { width: "33%" } }, + ], + }, + ], + }, + { + name: fields.events, + type: "array", + required: true, + minRows: 1, + fields: [ + { + name: fields.eventsSource, + type: "relationship", + relationTo: [Collections.Contents, Collections.LibraryItems], + // required: true, + admin: { allowCreate: false }, + }, + translatedFields({ + name: fields.eventsTranslations, + required: true, + minRows: 1, + admin: { + useAsTitle: fields.eventsTranslationsTitle, + hasSourceLanguage: true, + hasCredits: true, + }, + fields: [ + { + name: fields.eventsTranslationsTitle, + validate: (_, { siblingData: { description, title } }) => { + if (isEmpty(description) && isEmpty(title)) { + return "This field is required if no description is set."; + } + return true; + }, + type: "text", + }, + { + name: fields.eventsTranslationsDescription, + validate: (_, { siblingData: { description, title } }) => { + if (isEmpty(description) && isEmpty(title)) { + return "This field is required if no title is set."; + } + return true; + }, + type: "textarea", + }, + { name: fields.eventsTranslationsNotes, type: "textarea" }, + ], + }), + ], + }, + ], +}); diff --git a/src/collections/ChronologyItems/endpoints/importFromStrapi.ts b/src/collections/ChronologyItems/endpoints/importFromStrapi.ts index dfc44ca..a977d36 100644 --- a/src/collections/ChronologyItems/endpoints/importFromStrapi.ts +++ b/src/collections/ChronologyItems/endpoints/importFromStrapi.ts @@ -11,7 +11,7 @@ export const importFromStrapi = createStrapiImportEndpoint({ }, payload: { collection: Collections.ChronologyItems, - convert: ({ year, month, day, events }) => ({ + convert: ({ year, month, day, events }, user) => ({ date: { year, month, day }, events: events.map((event) => ({ translations: event.translations.map(({ title, description, note, language }) => ({ @@ -19,6 +19,10 @@ export const importFromStrapi = createStrapiImportEndpoint({ description, note, language: language.data.attributes.code, + sourceLanguage: "en", + ...(language.data.attributes.code === "en" + ? { transcribers: [user.id] } + : { translators: [user.id] }), })), })), }), diff --git a/src/collections/ChronologyItems/hooks/beforeValidatePopulateNameField.ts b/src/collections/ChronologyItems/hooks/beforeValidatePopulateNameField.ts new file mode 100644 index 0000000..6121ed2 --- /dev/null +++ b/src/collections/ChronologyItems/hooks/beforeValidatePopulateNameField.ts @@ -0,0 +1,20 @@ +import { FieldHook } from "payload/dist/fields/config/types"; +import { ChronologyItem } from "../../../types/collections"; +import { isDefined, isUndefined } from "../../../utils/asserts"; + +export const beforeValidatePopulateNameField: FieldHook< + ChronologyItem, + ChronologyItem["name"], + ChronologyItem +> = ({ data: { date } }) => { + if (isUndefined(date?.year)) return "????-??-??"; + const { year, month, day } = date; + let result = String(year).padStart(5, " "); + if (isDefined(month)) { + result += `-${String(date.month).padStart(2, "0")}`; + if (isDefined(day)) { + result += `-${String(date.day).padStart(2, "0")}`; + } + } + return result; +}; diff --git a/src/collections/Contents/Contents.ts b/src/collections/Contents/Contents.ts index 8a9e4b7..789ec1e 100644 --- a/src/collections/Contents/Contents.ts +++ b/src/collections/Contents/Contents.ts @@ -1,8 +1,9 @@ import { CollectionGroups, Collections, FileTypes, KeysTypes } from "../../constants"; import { fileField } from "../../fields/fileField/fileField"; import { imageField } from "../../fields/imageField/imageField"; +import { keysField } from "../../fields/keysField/keysField"; import { slugField } from "../../fields/slugField/slugField"; -import { localizedFields } from "../../fields/translatedFields/translatedFields"; +import { translatedFields } from "../../fields/translatedFields/translatedFields"; import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo"; import { beforeDuplicatePiping } from "../../hooks/beforeDuplicatePiping"; import { beforeDuplicateUnpublish } from "../../hooks/beforeDuplicateUnpublish"; @@ -33,190 +34,184 @@ const fields = { updatedBy: "updatedBy", } as const satisfies Record; -export const Contents = buildVersionedCollectionConfig( - Collections.Contents, - { +export const Contents = buildVersionedCollectionConfig({ + slug: Collections.Contents, + labels: { singular: "Content", plural: "Contents", }, - () => ({ - 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: CollectionGroups.Collections, - hooks: { - beforeDuplicate: beforeDuplicatePiping([ - beforeDuplicateUnpublish, - beforeDuplicateAddCopyTo(fields.slug), - ]), - }, - preview: (doc) => `https://accords-library.com/contents/${doc.slug}`, - }, - fields: [ - { - type: "row", - fields: [ - slugField({ name: fields.slug, admin: { width: "50%" } }), - imageField({ - name: fields.thumbnail, - relationTo: Collections.ContentsThumbnails, - admin: { width: "50%" }, - }), - ], - }, - { - type: "row", - fields: [ - { - name: fields.categories, - type: "relationship", - relationTo: [Collections.Keys], - filterOptions: { type: { equals: KeysTypes.Categories } }, - hasMany: true, - admin: { allowCreate: false, width: "50%" }, - }, - { - name: fields.type, - type: "relationship", - relationTo: [Collections.Keys], - 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: Collections.Recorders, - hasMany: true, - admin: { - condition: (_, siblingData) => - siblingData.language === siblingData.sourceLanguage, - width: "50%", - }, - }, - { - name: fields.textTranslators, - label: "Translators", - type: "relationship", - relationTo: Collections.Recorders, - hasMany: true, - admin: { - condition: (_, siblingData) => - siblingData.language !== siblingData.sourceLanguage, - width: "50%", - }, - }, - { - name: fields.textProofreaders, - label: "Proofreaders", - type: "relationship", - relationTo: Collections.Recorders, - 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%" }, - }, - ], - }, - ], - }, - ], - }, - ], - }), + 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: CollectionGroups.Collections, + hooks: { + beforeDuplicate: beforeDuplicatePiping([ + beforeDuplicateUnpublish, + beforeDuplicateAddCopyTo(fields.slug), + ]), + }, + preview: (doc) => `https://accords-library.com/contents/${doc.slug}`, + }, + fields: [ + { + type: "row", + fields: [ + slugField({ name: fields.slug, admin: { width: "50%" } }), + imageField({ + name: fields.thumbnail, + relationTo: Collections.ContentsThumbnails, + admin: { width: "50%" }, + }), + ], + }, + { + type: "row", + fields: [ + keysField({ + name: fields.categories, + relationTo: KeysTypes.Categories, + hasMany: true, + admin: { allowCreate: false, width: "50%" }, + }), + keysField({ + name: fields.type, + relationTo: KeysTypes.Contents, + admin: { allowCreate: false, width: "50%" }, + }), + ], + }, + translatedFields({ + 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: Collections.Recorders, + hasMany: true, + admin: { + condition: (_, siblingData) => + siblingData.language === siblingData.sourceLanguage, + width: "50%", + }, + }, + { + name: fields.textTranslators, + label: "Translators", + type: "relationship", + relationTo: Collections.Recorders, + hasMany: true, + admin: { + condition: (_, siblingData) => + siblingData.language !== siblingData.sourceLanguage, + width: "50%", + }, + }, + { + name: fields.textProofreaders, + label: "Proofreaders", + type: "relationship", + relationTo: Collections.Recorders, + 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/ContentsFolders/ContentsFolders.ts b/src/collections/ContentsFolders/ContentsFolders.ts index 8347f81..da9f23a 100644 --- a/src/collections/ContentsFolders/ContentsFolders.ts +++ b/src/collections/ContentsFolders/ContentsFolders.ts @@ -1,6 +1,6 @@ import { CollectionGroups, Collections } from "../../constants"; import { slugField } from "../../fields/slugField/slugField"; -import { localizedFields } from "../../fields/translatedFields/translatedFields"; +import { translatedFields } from "../../fields/translatedFields/translatedFields"; import { buildCollectionConfig } from "../../utils/collectionConfig"; const fields = { @@ -11,51 +11,49 @@ const fields = { contents: "contents", } as const satisfies Record; -export const ContentsFolders = buildCollectionConfig( - Collections.ContentsFolders, - { +export const ContentsFolders = buildCollectionConfig({ + slug: Collections.ContentsFolders, + labels: { singular: "Contents Folder", plural: "Contents Folders", }, - () => ({ - defaultSort: fields.slug, - admin: { - useAsTitle: fields.slug, - defaultColumns: [fields.slug, fields.translations], - disableDuplicate: true, - group: CollectionGroups.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: Collections.ContentsFolders, - hasMany: true, - admin: { width: "50%" }, - }, - { - type: "relationship", - name: fields.contents, - relationTo: Collections.Contents, - hasMany: true, - admin: { width: "50%" }, - }, - ], + defaultSort: fields.slug, + admin: { + useAsTitle: fields.slug, + defaultColumns: [fields.slug, fields.translations], + disableDuplicate: true, + group: CollectionGroups.Collections, + }, + timestamps: false, + versions: false, + fields: [ + slugField({ name: fields.slug }), + translatedFields({ + 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: Collections.ContentsFolders, + hasMany: true, + admin: { width: "50%" }, + }, + { + type: "relationship", + name: fields.contents, + relationTo: Collections.Contents, + hasMany: true, + admin: { width: "50%" }, + }, + ], + }, + ], +}); diff --git a/src/collections/ContentsThumbnails/ContentsThumbnails.ts b/src/collections/ContentsThumbnails/ContentsThumbnails.ts index 682a429..a6441d8 100644 --- a/src/collections/ContentsThumbnails/ContentsThumbnails.ts +++ b/src/collections/ContentsThumbnails/ContentsThumbnails.ts @@ -1,5 +1,5 @@ import { CollectionGroups, Collections } from "../../constants"; -import { buildCollectionConfig } from "../../utils/collectionConfig"; +import { buildImageCollectionConfig } from "../../utils/imageCollectionConfig"; const fields = { filename: "filename", @@ -7,43 +7,39 @@ const fields = { filesize: "filesize", } as const satisfies Record; -export const ContentsThumbnails = buildCollectionConfig( - Collections.ContentsThumbnails, - { +export const ContentsThumbnails = buildImageCollectionConfig({ + slug: Collections.ContentsThumbnails, + labels: { singular: "Contents Thumbnail", plural: "Contents Thumbnails", }, - ({ uploadDir }) => ({ - defaultSort: fields.filename, - admin: { - useAsTitle: fields.filename, - disableDuplicate: true, - group: CollectionGroups.Media, - }, - upload: { - staticDir: uploadDir, - mimeTypes: ["image/*"], - imageSizes: [ - { - name: "og", - height: 750, - width: 1125, - formatOptions: { - format: "jpg", - options: { progressive: true, mozjpeg: true, compressionLevel: 9, quality: 80 }, - }, + defaultSort: fields.filename, + admin: { + useAsTitle: fields.filename, + disableDuplicate: true, + group: CollectionGroups.Media, + }, + upload: { + 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/Currencies/Currencies.ts b/src/collections/Currencies/Currencies.ts index 1d6a484..30030d7 100644 --- a/src/collections/Currencies/Currencies.ts +++ b/src/collections/Currencies/Currencies.ts @@ -8,37 +8,35 @@ const fields = { id: "id", } as const satisfies Record; -export const Currencies = buildCollectionConfig( - Collections.Currencies, - { +export const Currencies = buildCollectionConfig({ + slug: Collections.Currencies, + labels: { singular: "Currency", plural: "Currencies", }, - () => ({ - defaultSort: fields.id, - admin: { - pagination: { defaultLimit: 100 }, - useAsTitle: fields.id, - defaultColumns: [fields.id], - disableDuplicate: true, - group: CollectionGroups.Meta, - }, - access: { create: mustBeAdmin, update: mustBeAdmin }, - endpoints: [importFromStrapi], - timestamps: false, - fields: [ - { - name: fields.id, - type: "text", - unique: true, - required: true, - validate: (value, options) => { - if (!/^[A-Z]{3}$/g.test(value)) { - return "The code must be a valid ISO 4217 currency code (e.g: EUR, CAD...)"; - } - return text(value, options); - }, + defaultSort: fields.id, + admin: { + pagination: { defaultLimit: 100 }, + useAsTitle: fields.id, + defaultColumns: [fields.id], + disableDuplicate: true, + group: CollectionGroups.Meta, + }, + access: { create: mustBeAdmin, update: mustBeAdmin }, + endpoints: [importFromStrapi], + timestamps: false, + fields: [ + { + name: fields.id, + type: "text", + unique: true, + required: true, + validate: (value, options) => { + if (!/^[A-Z]{3}$/g.test(value)) { + return "The code must be a valid ISO 4217 currency code (e.g: EUR, CAD...)"; + } + return text(value, options); }, - ], - }) -); + }, + ], +}); diff --git a/src/collections/Files/Files.ts b/src/collections/Files/Files.ts index 4046cab..3b855bb 100644 --- a/src/collections/Files/Files.ts +++ b/src/collections/Files/Files.ts @@ -6,31 +6,29 @@ const fields = { type: "type", } as const satisfies Record; -export const Files = buildCollectionConfig( - Collections.Files, - { +export const Files = buildCollectionConfig({ + slug: Collections.Files, + labels: { singular: "File", plural: "Files", }, - () => ({ - defaultSort: fields.filename, - admin: { - useAsTitle: fields.filename, - disableDuplicate: true, - group: CollectionGroups.Media, + defaultSort: fields.filename, + admin: { + useAsTitle: fields.filename, + disableDuplicate: true, + group: CollectionGroups.Media, + }, + fields: [ + { + name: fields.filename, + required: true, + type: "text", }, - fields: [ - { - name: fields.filename, - required: true, - type: "text", - }, - { - name: fields.type, - type: "select", - required: true, - options: Object.entries(FileTypes).map(([value, label]) => ({ label, value })), - }, - ], - }) -); + { + 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 c9a6824..6060e1a 100644 --- a/src/collections/Keys/Keys.ts +++ b/src/collections/Keys/Keys.ts @@ -2,7 +2,7 @@ import payload from "payload"; import { mustBeAdmin } from "../../accesses/mustBeAdmin"; import { QuickFilters } from "../../components/QuickFilters"; import { CollectionGroups, Collections, KeysTypes, LanguageCodes } from "../../constants"; -import { localizedFields } from "../../fields/translatedFields/translatedFields"; +import { translatedFields } from "../../fields/translatedFields/translatedFields"; import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo"; import { Key } from "../../types/collections"; import { isDefined } from "../../utils/asserts"; @@ -19,103 +19,101 @@ const fields = { const keysTypesWithShort: (keyof typeof KeysTypes)[] = ["Categories", "GamePlatforms"]; -export const Keys = buildCollectionConfig( - Collections.Keys, - { +export const Keys = buildCollectionConfig({ + slug: Collections.Keys, + labels: { singular: "Key", plural: "Keys", }, - () => ({ - defaultSort: fields.name, - admin: { - useAsTitle: fields.name, - defaultColumns: [fields.name, fields.type, fields.translations], - group: CollectionGroups.Meta, - components: { - BeforeListTable: [ - () => - QuickFilters({ - slug: Collections.Keys, - filterGroups: [ - Object.entries(KeysTypes).map(([key, value]) => ({ - label: value, - filter: { where: { type: { equals: key } } }, - })), - Object.entries(LanguageCodes).map(([key, value]) => ({ - label: `∅ ${value}`, - filter: { where: { "translations.language": { not_equals: key } } }, - })), - ], - }), - ], - }, - hooks: { - beforeDuplicate: beforeDuplicateAddCopyTo(fields.name), - }, - }, - access: { - create: mustBeAdmin, - delete: mustBeAdmin, - }, - hooks: { - beforeValidate: [ - async ({ data: { name, type } }) => { - const result = await payload.find({ - collection: Collections.Keys, - where: { name: { equals: name }, type: { equals: type } }, - }); - if (result.docs.length > 0) { - throw new Error( - `A Key of type "${KeysTypes[type]}" already exists with the name "${name}"` - ); - } - }, + defaultSort: fields.name, + admin: { + useAsTitle: fields.name, + defaultColumns: [fields.name, fields.type, fields.translations], + group: CollectionGroups.Meta, + components: { + BeforeListTable: [ + () => + QuickFilters({ + slug: Collections.Keys, + filterGroups: [ + Object.entries(KeysTypes).map(([key, value]) => ({ + label: value, + filter: { where: { type: { equals: key } } }, + })), + Object.entries(LanguageCodes).map(([key, value]) => ({ + label: `∅ ${value}`, + filter: { where: { "translations.language": { not_equals: key } } }, + })), + ], + }), ], }, - endpoints: [importFromStrapi], - timestamps: false, - versions: false, - fields: [ - { - name: fields.name, - type: "text", - required: true, + hooks: { + beforeDuplicate: beforeDuplicateAddCopyTo(fields.name), + }, + }, + access: { + create: mustBeAdmin, + delete: mustBeAdmin, + }, + hooks: { + beforeValidate: [ + async ({ data: { name, type } }) => { + const result = await payload.find({ + collection: Collections.Keys, + where: { name: { equals: name }, type: { equals: type } }, + }); + if (result.docs.length > 0) { + throw new Error( + `A Key of type "${KeysTypes[type]}" already exists with the name "${name}"` + ); + } }, - { - name: fields.type, - type: "select", - required: true, - options: Object.entries(KeysTypes).map(([value, label]) => ({ label, value })), - }, - localizedFields({ - name: fields.translations, - interfaceName: "CategoryTranslations", - admin: { - useAsTitle: fields.translationsName, - }, - fields: [ - { - type: "row", - fields: [ - { - name: fields.translationsName, - type: "text", - required: true, - admin: { width: "50%" }, - }, - { - name: fields.translationsShort, - type: "text", - admin: { - condition: (data: Partial) => - isDefined(data.type) && keysTypesWithShort.includes(data.type), - width: "50%", - }, - }, - ], - }, - ], - }), ], - }) -); + }, + endpoints: [importFromStrapi], + timestamps: false, + versions: false, + fields: [ + { + name: fields.name, + type: "text", + required: true, + }, + { + name: fields.type, + type: "select", + required: true, + options: Object.entries(KeysTypes).map(([value, label]) => ({ label, value })), + }, + translatedFields({ + name: fields.translations, + interfaceName: "CategoryTranslations", + admin: { + useAsTitle: fields.translationsName, + }, + fields: [ + { + type: "row", + fields: [ + { + name: fields.translationsName, + type: "text", + required: true, + admin: { width: "50%" }, + }, + { + name: fields.translationsShort, + type: "text", + admin: { + condition: (data: Partial) => + isDefined(data.type) && keysTypesWithShort.includes(data.type), + width: "50%", + }, + }, + ], + }, + ], + }), + ], +}); diff --git a/src/collections/Languages/Languages.ts b/src/collections/Languages/Languages.ts index fc7556c..06c9d2f 100644 --- a/src/collections/Languages/Languages.ts +++ b/src/collections/Languages/Languages.ts @@ -10,43 +10,42 @@ const fields = { name: "name", } as const satisfies Record; -export const Languages = buildCollectionConfig( - Collections.Languages, - { +export const Languages = buildCollectionConfig({ + slug: Collections.Languages, + labels: { singular: "Language", plural: "Languages", }, - () => ({ - defaultSort: fields.name, - admin: { - useAsTitle: fields.name, - defaultColumns: [fields.name, fields.id], - disableDuplicate: true, - group: CollectionGroups.Meta, - pagination: { defaultLimit: 100 }, + defaultSort: fields.name, + admin: { + useAsTitle: fields.name, + defaultColumns: [fields.name, fields.id], + disableDuplicate: true, + group: CollectionGroups.Meta, + pagination: { defaultLimit: 100 }, + }, + access: { create: mustBeAdmin, update: mustBeAdmin, read: publicAccess }, + timestamps: false, + endpoints: [importFromStrapi], + fields: [ + { + name: fields.id, + type: "text", + unique: true, + required: true, + validate: (value, options) => { + if (!/^[a-z]{2}(-[a-z]{2})?$/g.test(value)) { + return "The code must be a valid BCP 47 language \ + tag and lowercase (i.e: en, pt-pt, fr, zh-tw...)"; + } + return text(value, options); + }, }, - access: { create: mustBeAdmin, update: mustBeAdmin, read: publicAccess }, - timestamps: false, - endpoints: [importFromStrapi], - fields: [ - { - name: fields.id, - type: "text", - unique: true, - required: true, - validate: (value, options) => { - if (!/^[a-z]{2}(-[a-z]{2})?$/g.test(value)) { - return "The code must be a valid BCP 47 language tag and lowercase (i.e: en, pt-pt, fr, zh-tw...)"; - } - return text(value, options); - }, - }, - { - name: fields.name, - type: "text", - unique: true, - required: true, - }, - ], - }) -); + { + name: fields.name, + type: "text", + unique: true, + required: true, + }, + ], +}); diff --git a/src/collections/LibraryItems/LibraryItems.ts b/src/collections/LibraryItems/LibraryItems.ts index 4c9ebe9..331a4ec 100644 --- a/src/collections/LibraryItems/LibraryItems.ts +++ b/src/collections/LibraryItems/LibraryItems.ts @@ -7,35 +7,55 @@ import { LibraryItemsTypes, } from "../../constants"; import { imageField } from "../../fields/imageField/imageField"; +import { keysField } from "../../fields/keysField/keysField"; import { optionalGroupField } from "../../fields/optionalGroupField/optionalGroupField"; import { slugField } from "../../fields/slugField/slugField"; +import { translatedFields } from "../../fields/translatedFields/translatedFields"; import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo"; import { beforeDuplicatePiping } from "../../hooks/beforeDuplicatePiping"; import { beforeDuplicateUnpublish } from "../../hooks/beforeDuplicateUnpublish"; import { LibraryItem } from "../../types/collections"; +import { isDefined } from "../../utils/asserts"; import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig"; import { RowLabel } from "./components/RowLabel"; import { getBySlug } from "./endpoints/getBySlug"; const fields = { - status: "status", + status: "_status", + slug: "slug", thumbnail: "thumbnail", + pretitle: "pretitle", title: "title", subtitle: "subtitle", + + translations: "translations", + translationsDescription: "description", + rootItem: "rootItem", primary: "primary", digital: "digital", downloadable: "downloadable", + size: "size", width: "width", height: "height", thickness: "thickness", + price: "price", priceAmount: "amount", priceCurrency: "currency", releaseDate: "releaseDate", + + gallery: "gallery", + galleryImage: "image", + + urls: "urls", + urlsUrl: "url", + + categories: "categories", + itemType: "itemType", textual: "textual", textualSubtype: "subtype", @@ -43,24 +63,51 @@ const fields = { textualPageCount: "pageCount", textualPageOrder: "pageOrder", textualLanguages: "languages", + audio: "audio", audioSubtype: "audioSubtype", + scans: "scans", + scansCover: "cover", + scansCoverFlapFront: "flapFront", scansCoverFront: "front", scansCoverSpine: "spine", scansCoverBack: "back", + scansCoverFlapBack: "flapBack", + scansCoverInsideFlapFront: "insideFlapFront", + scansCoverInsideFront: "insideFront", + scansCoverInsideBack: "insideBack", + scansCoverInsideFlapBack: "insideFlapBack", + scansDustjacket: "dustjacket", + scansDustjacketFlapFront: "flapFront", scansDustjacketFront: "front", scansDustjacketSpine: "spine", scansDustjacketBack: "back", + scansDustjacketFlapBack: "flapBack", + scansDustjacketInsideFlapFront: "insideFlapFront", + scansDustjacketInsideFront: "insideFront", + scansDustjacketInsideSpine: "insideSpine", + scansDustjacketInsideBack: "insideBack", + scansDustjacketInsideFlapBack: "insideFlapBack", + scansObi: "obi", + scansObiFlapFront: "flapFront", scansObiFront: "front", scansObiSpine: "spine", scansObiBack: "back", + scansObiFlapBack: "flapBack", + scansObiInsideFlapFront: "insideFlapFront", + scansObiInsideFront: "insideFront", + scansObiInsideSpine: "insideSpine", + scansObiInsideBack: "insideBack", + scansObiInsideFlapBack: "insideFlapBack", + scansPages: "pages", scansPagesPage: "page", scansPagesImage: "image", + contents: "contents", contentsContent: "content", contentsPageStart: "pageStart", @@ -70,428 +117,636 @@ const fields = { contentsNote: "note", } as const satisfies Record; -export const LibraryItems = buildVersionedCollectionConfig( - Collections.LibraryItems, - { +export const LibraryItems = buildVersionedCollectionConfig({ + slug: Collections.LibraryItems, + labels: { singular: "Library Item", plural: "Library Items", }, - () => ({ - defaultSort: fields.slug, - admin: { - useAsTitle: fields.slug, - description: - "A comprehensive list of all Yokoverse’s side materials (books, novellas, artbooks, \ + 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: CollectionGroups.Collections, - hooks: { - beforeDuplicate: beforeDuplicatePiping([ - beforeDuplicateUnpublish, - beforeDuplicateAddCopyTo(fields.slug), - ]), - }, - preview: (doc) => `https://accords-library.com/library/${doc.slug}`, + defaultColumns: [fields.slug, fields.thumbnail, fields.status], + group: CollectionGroups.Collections, + hooks: { + beforeDuplicate: beforeDuplicatePiping([ + beforeDuplicateUnpublish, + beforeDuplicateAddCopyTo(fields.slug), + ]), }, - endpoints: [getBySlug], - fields: [ - { - type: "row", - fields: [ - slugField({ name: fields.slug, admin: { width: "50%" } }), - imageField({ - name: fields.thumbnail, - relationTo: Collections.LibraryItemsThumbnails, - admin: { width: "50%" }, - }), - ], + preview: (doc) => `https://accords-library.com/library/${doc.slug}`, + }, + endpoints: [getBySlug], + fields: [ + { + name: fields.itemType, + type: "radio", + options: Object.entries(LibraryItemsTypes).map(([value, label]) => ({ + label, + value, + })), + admin: { + layout: "horizontal", }, - { - type: "row", - fields: [ - { name: fields.pretitle, type: "text" }, - { name: fields.title, type: "text", required: true }, - { name: fields.subtitle, type: "text" }, - ], + }, + { + type: "tabs", + admin: { + condition: ({ itemType }) => isDefined(itemType), }, - { - 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%", + tabs: [ + { + label: "Overview", + fields: [ + { + type: "row", + fields: [ + slugField({ + name: fields.slug, + admin: { width: "50%" }, + }), + imageField({ + name: fields.thumbnail, + relationTo: Collections.LibraryItemsThumbnails, + admin: { 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.pretitle, type: "text" }, + { name: fields.title, type: "text", required: true }, + { name: fields.subtitle, type: "text" }, + ], }, - }, - { - 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%", - }, - }, - ], - }, - optionalGroupField({ - name: fields.scans, - fields: [ - optionalGroupField({ - name: fields.scansCover, - fields: [ - { - type: "row", - fields: [ - imageField({ - name: fields.scansCoverFront, - relationTo: Collections.LibraryItemsThumbnails, - admin: { width: "33%" }, - }), - imageField({ - name: fields.scansCoverSpine, - relationTo: Collections.LibraryItemsThumbnails, - admin: { width: "33%" }, - }), - imageField({ - name: fields.scansCoverBack, - relationTo: Collections.LibraryItemsThumbnails, - admin: { width: "33%" }, - }), - ], - }, - ], - }), - optionalGroupField({ - name: fields.scansDustjacket, - label: "Dust Jacket", - labels: { singular: "Dust Jacket", plural: "Dust Jackets" }, - admin: { - description: - "The dust jacket of a book is the detachable outer cover with folded \ - flaps that hold it to the front and back book covers", - }, - fields: [ - { - type: "row", - fields: [ - imageField({ - name: fields.scansDustjacketFront, - relationTo: Collections.LibraryItemsThumbnails, - admin: { width: "33%" }, - }), - imageField({ - name: fields.scansDustjacketSpine, - relationTo: Collections.LibraryItemsThumbnails, - admin: { width: "33%" }, - }), - imageField({ - name: fields.scansDustjacketBack, - relationTo: Collections.LibraryItemsThumbnails, - admin: { width: "33%" }, - }), - ], - }, - ], - }), - optionalGroupField({ - name: fields.scansObi, - label: "Obi", - labels: { singular: "Obi Belt", plural: "Obi Belts" }, - admin: { - description: - "An obi is a strip of paper looped around a book or other product. \ - it typically add marketing claims, or other relevant information about the product.", - }, - fields: [ - { - type: "row", - fields: [ - imageField({ - name: fields.scansObiFront, - relationTo: Collections.LibraryItemsThumbnails, - admin: { width: "33%" }, - }), - imageField({ - name: fields.scansObiSpine, - relationTo: Collections.LibraryItemsThumbnails, - admin: { width: "33%" }, - }), - imageField({ - name: fields.scansObiBack, - relationTo: Collections.LibraryItemsThumbnails, - admin: { width: "33%" }, - }), - ], - }, - ], - }), - { - name: fields.scansPages, - type: "array", - admin: { - initCollapsed: true, - description: - "Make sure the page number corresponds to the page number written on \ - the scan. You can use negative page numbers if necessary.", - components: { - RowLabel: ({ data }) => RowLabel(data), - }, - }, - fields: [ - { - type: "row", - fields: [ - { - name: fields.scansPagesPage, - type: "number", - required: true, - admin: { width: "33%" }, + { + 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%", }, - imageField({ - name: fields.scansPagesImage, - relationTo: Collections.LibraryItemsThumbnails, - required: true, - admin: { width: "66%" }, - }), - ], - }, - ], - }, - ], - }), - optionalGroupField({ - name: fields.size, - admin: { condition: (data) => !data.digital }, - fields: [ - { - type: "row", - fields: [ - { - name: fields.width, - type: "number", - required: true, - admin: { step: 1, width: "33%", description: "in mm." }, - }, - { - name: fields.height, - type: "number", - required: true, - admin: { step: 1, width: "33%", description: "in mm." }, - }, - { - name: fields.thickness, - type: "number", - admin: { step: 1, width: "33%", description: "in mm." }, - }, - ], - }, - ], - }), - optionalGroupField({ - name: fields.price, - admin: { className: "group-array" }, - fields: [ - { - type: "row", - fields: [ - { - name: fields.priceAmount, - type: "number", - required: true, - min: 0, - admin: { width: "50%" }, - }, - { - name: fields.priceCurrency, - type: "relationship", - relationTo: Collections.Currencies, - required: 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.textual, - type: "group", - admin: { - condition: (data: Partial) => data.itemType === LibraryItemsTypes.Textual, - }, - fields: [ - { - type: "row", - fields: [ - { - name: fields.textualSubtype, - label: "Subtype", - type: "relationship", - relationTo: [Collections.Keys], - filterOptions: { type: { equals: KeysTypes.LibraryTextual } }, - hasMany: true, - admin: { allowCreate: false, width: "50%" }, - }, - { - name: fields.textualLanguages, - type: "relationship", - relationTo: [Collections.Languages], - 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.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.audio, - type: "group", - admin: { - condition: (data: Partial) => data.itemType === LibraryItemsTypes.Audio, + { + 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%", + }, + }, + ], + }, + ], }, - fields: [ - { - type: "row", - fields: [ - { - name: fields.audioSubtype, - label: "Subtype", - type: "relationship", - relationTo: [Collections.Keys], - filterOptions: { type: { equals: KeysTypes.LibraryAudio } }, - hasMany: true, - admin: { allowCreate: false, width: "50%" }, - }, - ], - }, - ], - }, - { - name: fields.contents, - type: "array", - fields: [ - { - name: fields.contentsContent, - type: "relationship", - relationTo: Collections.Contents, - required: true, - }, - { - type: "row", - admin: { - condition: ({ itemType }) => { - return itemType === LibraryItemsTypes.Textual; + { + label: "Images", + fields: [ + { + name: fields.gallery, + type: "array", + admin: { + description: + "Additional images of the item (unboxing, on shelf, promotional images...)", }, + fields: [ + imageField({ + name: fields.galleryImage, + relationTo: Collections.LibraryItemsGallery, + }), + ], }, - fields: [ - { - name: fields.contentsPageStart, - type: "number", - }, - { name: fields.contentsPageEnd, type: "number" }, - ], - }, - { - type: "row", - admin: { - condition: ({ itemType }) => { - return itemType === LibraryItemsTypes.Audio || itemType === LibraryItemsTypes.Video; - }, - }, - fields: [ - { - name: fields.contentsTimeStart, - type: "number", - }, - { name: fields.contentsTimeEnd, type: "number" }, - ], - }, - { - name: fields.contentsNote, - type: "textarea", - admin: { - condition: ({ itemType }) => { - return itemType === LibraryItemsTypes.Game || itemType === LibraryItemsTypes.Other; - }, - }, - }, - ], - }, - { - name: fields.releaseDate, - type: "date", - admin: { - date: { pickerAppearance: "dayOnly", displayFormat: "yyyy-MM-dd" }, - position: "sidebar", + optionalGroupField({ + name: fields.scans, + fields: [ + optionalGroupField({ + name: fields.scansCover, + fields: [ + { + type: "row", + fields: [ + imageField({ + name: fields.scansCoverFront, + relationTo: Collections.LibraryItemsScans, + admin: { width: "33%" }, + }), + imageField({ + name: fields.scansCoverSpine, + relationTo: Collections.LibraryItemsScans, + admin: { width: "33%" }, + }), + imageField({ + name: fields.scansCoverBack, + relationTo: Collections.LibraryItemsScans, + admin: { width: "33%" }, + }), + ], + }, + { + type: "row", + fields: [ + imageField({ + name: fields.scansCoverInsideFront, + relationTo: Collections.LibraryItemsScans, + admin: { width: "50%" }, + }), + imageField({ + name: fields.scansCoverBack, + relationTo: Collections.LibraryItemsScans, + admin: { width: "50%" }, + }), + ], + }, + { + type: "row", + fields: [ + imageField({ + name: fields.scansCoverFlapFront, + relationTo: Collections.LibraryItemsScans, + admin: { width: "25%" }, + }), + imageField({ + name: fields.scansCoverFlapBack, + relationTo: Collections.LibraryItemsScans, + admin: { width: "25%" }, + }), + imageField({ + name: fields.scansCoverInsideFlapFront, + relationTo: Collections.LibraryItemsScans, + admin: { width: "25%" }, + }), + imageField({ + name: fields.scansCoverInsideFlapBack, + relationTo: Collections.LibraryItemsScans, + admin: { width: "25%" }, + }), + ], + }, + ], + }), + optionalGroupField({ + name: fields.scansDustjacket, + label: "Dust Jacket", + labels: { singular: "Dust Jacket", plural: "Dust Jackets" }, + admin: { + description: + "The dust jacket of a book is the detachable outer cover with folded \ + flaps that hold it to the front and back book covers", + }, + fields: [ + { + type: "row", + fields: [ + imageField({ + name: fields.scansDustjacketFront, + relationTo: Collections.LibraryItemsScans, + admin: { width: "33%" }, + }), + imageField({ + name: fields.scansDustjacketSpine, + relationTo: Collections.LibraryItemsScans, + admin: { width: "33%" }, + }), + imageField({ + name: fields.scansDustjacketBack, + relationTo: Collections.LibraryItemsScans, + admin: { width: "33%" }, + }), + ], + }, + { + type: "row", + fields: [ + imageField({ + name: fields.scansDustjacketInsideFront, + relationTo: Collections.LibraryItemsScans, + admin: { width: "33%" }, + }), + imageField({ + name: fields.scansDustjacketInsideSpine, + relationTo: Collections.LibraryItemsScans, + admin: { width: "33%" }, + }), + imageField({ + name: fields.scansDustjacketInsideBack, + relationTo: Collections.LibraryItemsScans, + admin: { width: "33%" }, + }), + ], + }, + { + type: "row", + fields: [ + imageField({ + name: fields.scansDustjacketFlapFront, + relationTo: Collections.LibraryItemsScans, + admin: { width: "25%" }, + }), + imageField({ + name: fields.scansDustjacketFlapBack, + relationTo: Collections.LibraryItemsScans, + admin: { width: "25%" }, + }), + imageField({ + name: fields.scansDustjacketInsideFlapFront, + relationTo: Collections.LibraryItemsScans, + admin: { width: "25%" }, + }), + imageField({ + name: fields.scansDustjacketInsideFlapBack, + relationTo: Collections.LibraryItemsScans, + admin: { width: "25%" }, + }), + ], + }, + ], + }), + optionalGroupField({ + name: fields.scansObi, + label: "Obi", + labels: { singular: "Obi Belt", plural: "Obi Belts" }, + admin: { + description: + "An obi is a strip of paper looped around a book or other product. \ + it typically add marketing claims, or other relevant information about the product.", + }, + fields: [ + { + type: "row", + fields: [ + imageField({ + name: fields.scansObiFront, + relationTo: Collections.LibraryItemsScans, + admin: { width: "33%" }, + }), + imageField({ + name: fields.scansObiSpine, + relationTo: Collections.LibraryItemsScans, + admin: { width: "33%" }, + }), + imageField({ + name: fields.scansObiBack, + relationTo: Collections.LibraryItemsScans, + admin: { width: "33%" }, + }), + ], + }, + { + type: "row", + fields: [ + imageField({ + name: fields.scansObiInsideFront, + relationTo: Collections.LibraryItemsScans, + admin: { width: "33%" }, + }), + imageField({ + name: fields.scansObiInsideSpine, + relationTo: Collections.LibraryItemsScans, + admin: { width: "33%" }, + }), + imageField({ + name: fields.scansObiInsideBack, + relationTo: Collections.LibraryItemsScans, + admin: { width: "33%" }, + }), + ], + }, + { + type: "row", + fields: [ + imageField({ + name: fields.scansObiFlapFront, + relationTo: Collections.LibraryItemsScans, + admin: { width: "25%" }, + }), + imageField({ + name: fields.scansObiFlapBack, + relationTo: Collections.LibraryItemsScans, + admin: { width: "25%" }, + }), + imageField({ + name: fields.scansObiInsideFlapFront, + relationTo: Collections.LibraryItemsScans, + admin: { width: "25%" }, + }), + imageField({ + name: fields.scansObiInsideFlapBack, + relationTo: Collections.LibraryItemsScans, + admin: { width: "25%" }, + }), + ], + }, + ], + }), + { + name: fields.scansPages, + type: "array", + admin: { + initCollapsed: true, + description: + "Make sure the page number corresponds to the page number written on \ + the scan. You can use negative page numbers if necessary.", + components: { + RowLabel: ({ data }) => RowLabel(data), + }, + }, + fields: [ + { + type: "row", + fields: [ + { + name: fields.scansPagesPage, + type: "number", + required: true, + admin: { width: "33%" }, + }, + imageField({ + name: fields.scansPagesImage, + relationTo: Collections.LibraryItemsScans, + required: true, + admin: { width: "66%" }, + }), + ], + }, + ], + }, + ], + }), + ], }, - }, - ], - }) -); + { + label: "Type", + admin: { condition: () => false }, + fields: [ + { + name: fields.textual, + type: "group", + admin: { + condition: (data: Partial) => + data.itemType === LibraryItemsTypes.Textual, + }, + fields: [ + { + type: "row", + fields: [ + keysField({ + name: fields.textualSubtype, + relationTo: KeysTypes.LibraryTextual, + hasMany: true, + admin: { allowCreate: false, width: "50%" }, + }), + { + name: fields.textualLanguages, + type: "relationship", + relationTo: Collections.Languages, + hasMany: true, + admin: { allowCreate: false, width: "50%" }, + }, + ], + }, + { + type: "row", + fields: [ + { + name: fields.textualPageCount, + type: "number", + min: 1, + admin: { width: "33%" }, + }, + { + name: fields.textualBindingType, + type: "radio", + options: Object.entries(LibraryItemsTextualBindingTypes).map( + ([value, label]) => ({ + label, + value, + }) + ), + admin: { + layout: "horizontal", + width: "33%", + }, + }, + { + name: fields.textualPageOrder, + 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: [ + keysField({ + name: fields.audioSubtype, + relationTo: KeysTypes.LibraryAudio, + hasMany: true, + admin: { allowCreate: false, width: "50%" }, + }), + ], + }, + ], + }, + ], + }, + { + label: "Details", + fields: [ + { + type: "row", + fields: [ + { + name: fields.releaseDate, + type: "date", + admin: { + date: { pickerAppearance: "dayOnly", displayFormat: "yyyy-MM-dd" }, + width: "50%", + }, + }, + keysField({ + name: fields.categories, + relationTo: KeysTypes.Categories, + hasMany: true, + admin: { allowCreate: false, width: "50%" }, + }), + ], + }, + translatedFields({ + name: fields.translations, + label: "Descriptions", + admin: { initCollapsed: true }, + fields: [{ name: fields.translationsDescription, type: "textarea", required: true }], + }), + optionalGroupField({ + name: fields.size, + admin: { condition: (data) => !data.digital }, + fields: [ + { + type: "row", + fields: [ + { + name: fields.width, + type: "number", + required: true, + admin: { step: 1, width: "33%", description: "in mm." }, + }, + { + name: fields.height, + type: "number", + required: true, + admin: { step: 1, width: "33%", description: "in mm." }, + }, + { + name: fields.thickness, + type: "number", + admin: { step: 1, width: "33%", description: "in mm." }, + }, + ], + }, + ], + }), + optionalGroupField({ + name: fields.price, + admin: { className: "group-array", width: "50%" }, + fields: [ + { + type: "row", + fields: [ + { + name: fields.priceAmount, + type: "number", + required: true, + min: 0, + admin: { width: "50%" }, + }, + { + name: fields.priceCurrency, + type: "relationship", + relationTo: Collections.Currencies, + required: true, + admin: { allowCreate: false, width: "50%" }, + }, + ], + }, + ], + }), + { + name: fields.urls, + label: "URLs", + type: "array", + admin: { + description: "Links to official websites where to get/buy the item.", + width: "50%", + }, + fields: [{ name: fields.urlsUrl, type: "text", required: true }], + }, + ], + }, + { + label: "Contents", + fields: [ + { + name: fields.contents, + type: "array", + fields: [ + { + name: fields.contentsContent, + type: "relationship", + relationTo: Collections.Contents, + required: true, + }, + { + type: "row", + admin: { + condition: ({ itemType }) => itemType === LibraryItemsTypes.Textual, + }, + fields: [ + { + name: fields.contentsPageStart, + type: "number", + }, + { name: fields.contentsPageEnd, type: "number" }, + ], + }, + { + type: "row", + admin: { + condition: ({ itemType }) => + itemType === LibraryItemsTypes.Audio || itemType === LibraryItemsTypes.Video, + }, + fields: [ + { + name: fields.contentsTimeStart, + type: "number", + }, + { name: fields.contentsTimeEnd, type: "number" }, + ], + }, + { + name: fields.contentsNote, + type: "textarea", + admin: { + condition: ({ itemType }) => + itemType === LibraryItemsTypes.Game || itemType === LibraryItemsTypes.Other, + }, + }, + ], + }, + ], + }, + ], + }, + ], +}); diff --git a/src/collections/LibraryItemsGallery/LibraryItemsGallery.ts b/src/collections/LibraryItemsGallery/LibraryItemsGallery.ts new file mode 100644 index 0000000..500e554 --- /dev/null +++ b/src/collections/LibraryItemsGallery/LibraryItemsGallery.ts @@ -0,0 +1,44 @@ +import { CollectionGroups, Collections } from "../../constants"; +import { buildImageCollectionConfig } from "../../utils/imageCollectionConfig"; + +const fields = { + filename: "filename", + mimeType: "mimeType", + filesize: "filesize", +} as const satisfies Record; + +export const LibraryItemsGallery = buildImageCollectionConfig({ + slug: Collections.LibraryItemsGallery, + labels: { + singular: "Library Item Gallery", + plural: "Library Item Gallery", + }, + defaultSort: fields.filename, + admin: { + useAsTitle: fields.filename, + disableDuplicate: true, + group: CollectionGroups.Media, + }, + upload: { + imageSizes: [ + { + name: "small", + height: 512, + width: 512, + fit: "cover", + formatOptions: { + format: "webp", + options: { effort: 6, quality: 60, alphaQuality: 60 }, + }, + }, + { + name: "max", + formatOptions: { + format: "webp", + options: { effort: 6, quality: 80, alphaQuality: 80 }, + }, + }, + ], + }, + fields: [], +}); diff --git a/src/collections/LibraryItemsScans/LibraryItemsScans.ts b/src/collections/LibraryItemsScans/LibraryItemsScans.ts new file mode 100644 index 0000000..af019ec --- /dev/null +++ b/src/collections/LibraryItemsScans/LibraryItemsScans.ts @@ -0,0 +1,57 @@ +import { CollectionGroups, Collections } from "../../constants"; +import { buildImageCollectionConfig } from "../../utils/imageCollectionConfig"; + +const fields = { + filename: "filename", + mimeType: "mimeType", + filesize: "filesize", +} as const satisfies Record; + +export const LibraryItemsScans = buildImageCollectionConfig({ + slug: Collections.LibraryItemsScans, + labels: { + singular: "Library Item Scans", + plural: "Library Item Scans", + }, + defaultSort: fields.filename, + admin: { + useAsTitle: fields.filename, + disableDuplicate: true, + group: CollectionGroups.Media, + }, + upload: { + 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/LibraryItemsThumbnails/LibraryItemsThumbnails.ts b/src/collections/LibraryItemsThumbnails/LibraryItemsThumbnails.ts index dba6ffc..8b2758e 100644 --- a/src/collections/LibraryItemsThumbnails/LibraryItemsThumbnails.ts +++ b/src/collections/LibraryItemsThumbnails/LibraryItemsThumbnails.ts @@ -1,5 +1,5 @@ import { CollectionGroups, Collections } from "../../constants"; -import { buildCollectionConfig } from "../../utils/collectionConfig"; +import { buildImageCollectionConfig } from "../../utils/imageCollectionConfig"; const fields = { filename: "filename", @@ -7,55 +7,49 @@ const fields = { filesize: "filesize", } as const satisfies Record; -export const LibraryItemsThumbnails = buildCollectionConfig( - Collections.LibraryItemsThumbnails, - { +export const LibraryItemsThumbnails = buildImageCollectionConfig({ + slug: Collections.LibraryItemsThumbnails, + labels: { singular: "Library Item Thumbnail", plural: "Library Item Thumbnails", }, - ({ uploadDir }) => ({ - defaultSort: fields.filename, - admin: { - useAsTitle: fields.filename, - disableDuplicate: true, - group: CollectionGroups.Media, - }, - upload: { - staticDir: uploadDir, - mimeTypes: ["image/*"], - imageSizes: [ - { - name: "og", - height: 1024, - width: 1024, - fit: "contain", - formatOptions: { - format: "jpg", - options: { progressive: true, mozjpeg: true, compressionLevel: 9, quality: 80 }, - }, + defaultSort: fields.filename, + admin: { + useAsTitle: fields.filename, + disableDuplicate: true, + group: CollectionGroups.Media, + }, + upload: { + imageSizes: [ + { + name: "og", + height: 1024, + width: 1024, + fit: "inside", + 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: "square", + height: 1024, + width: 1024, + fit: "contain", + background: { r: 0, g: 0, b: 0, alpha: 0 }, + 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: "max", + 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 bb70d3f..94fced0 100644 --- a/src/collections/Posts/Posts.ts +++ b/src/collections/Posts/Posts.ts @@ -2,7 +2,7 @@ import { QuickFilters, publishStatusFilters } from "../../components/QuickFilter import { CollectionGroups, Collections, KeysTypes } from "../../constants"; import { imageField } from "../../fields/imageField/imageField"; import { slugField } from "../../fields/slugField/slugField"; -import { localizedFields } from "../../fields/translatedFields/translatedFields"; +import { translatedFields } from "../../fields/translatedFields/translatedFields"; import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo"; import { beforeDuplicatePiping } from "../../hooks/beforeDuplicatePiping"; import { beforeDuplicateUnpublish } from "../../hooks/beforeDuplicateUnpublish"; @@ -26,151 +26,146 @@ const fields = { proofreaders: "proofreaders", } as const satisfies Record; -export const Posts = buildVersionedCollectionConfig( - Collections.Posts, - { +export const Posts = buildVersionedCollectionConfig({ + slug: Collections.Posts, + labels: { singular: "Post", plural: "Posts", }, - () => ({ - defaultSort: fields.slug, - admin: { - useAsTitle: fields.slug, - description: - "News articles written by our Recorders! Here you will find announcements about \ + 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: CollectionGroups.Collections, - components: { - BeforeListTable: [ - () => - QuickFilters({ - slug: Collections.Posts, - filterGroups: [publishStatusFilters], - }), - ], - }, - hooks: { - beforeDuplicate: beforeDuplicatePiping([ - beforeDuplicateUnpublish, - beforeDuplicateAddCopyTo(fields.slug), - ]), - }, - preview: (doc) => `https://accords-library.com/news/${doc.slug}`, + defaultColumns: [fields.slug, fields.thumbnail, fields.categories], + group: CollectionGroups.Collections, + components: { + BeforeListTable: [ + () => + QuickFilters({ + slug: Collections.Posts, + filterGroups: [publishStatusFilters], + }), + ], }, hooks: { - beforeValidate: [removeTranslatorsForTranscripts], + beforeDuplicate: beforeDuplicatePiping([ + beforeDuplicateUnpublish, + beforeDuplicateAddCopyTo(fields.slug), + ]), }, - fields: [ - { - type: "row", - fields: [ - slugField({ name: fields.slug, admin: { width: "50%" } }), - imageField({ - name: fields.thumbnail, - relationTo: Collections.PostsThumbnails, - admin: { width: "50%" }, - }), - ], - }, - { - type: "row", - fields: [ - { - name: fields.authors, - type: "relationship", - relationTo: [Collections.Recorders], - required: true, - minRows: 1, - hasMany: true, - admin: { width: "35%" }, - }, - { - name: fields.categories, - type: "relationship", - relationTo: [Collections.Keys], - 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: Collections.Recorders, - hasMany: true, - admin: { - condition: (_, siblingData) => { - if ( - isUndefined(siblingData.language) || - isUndefined(siblingData.sourceLanguage) - ) { - return false; - } - return siblingData.language !== siblingData.sourceLanguage; - }, - width: "50%", - }, - validate: (translators, { siblingData }) => { + preview: (doc) => `https://accords-library.com/news/${doc.slug}`, + }, + hooks: { + beforeValidate: [removeTranslatorsForTranscripts], + }, + fields: [ + { + type: "row", + fields: [ + slugField({ name: fields.slug, admin: { width: "50%" } }), + imageField({ + name: fields.thumbnail, + relationTo: Collections.PostsThumbnails, + admin: { width: "50%" }, + }), + ], + }, + { + type: "row", + fields: [ + { + name: fields.authors, + type: "relationship", + relationTo: [Collections.Recorders], + required: true, + minRows: 1, + hasMany: true, + admin: { width: "35%" }, + }, + { + name: fields.categories, + type: "relationship", + relationTo: [Collections.Keys], + filterOptions: { type: { equals: KeysTypes.Categories } }, + hasMany: true, + admin: { allowCreate: false, width: "35%" }, + }, + ], + }, + translatedFields({ + 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: Collections.Recorders, + hasMany: true, + admin: { + condition: (_, siblingData) => { if ( isUndefined(siblingData.language) || isUndefined(siblingData.sourceLanguage) ) { - return true; + return false; } - 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."; + return siblingData.language !== siblingData.sourceLanguage; }, + width: "50%", }, - { - name: fields.proofreaders, - type: "relationship", - relationTo: Collections.Recorders, - hasMany: true, - admin: { 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.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.proofreaders, + type: "relationship", + relationTo: Collections.Recorders, + hasMany: true, + admin: { width: "50%" }, + }, + ], }, - required: true, + { 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.hidden, - type: "checkbox", - required: false, - defaultValue: false, - admin: { - description: "If enabled, the post won't appear in the 'News' section", - position: "sidebar", - }, + 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/PostsThumbnails/PostsThumbnails.ts b/src/collections/PostsThumbnails/PostsThumbnails.ts index 0ce2b1c..91a9993 100644 --- a/src/collections/PostsThumbnails/PostsThumbnails.ts +++ b/src/collections/PostsThumbnails/PostsThumbnails.ts @@ -1,5 +1,5 @@ import { CollectionGroups, Collections } from "../../constants"; -import { buildCollectionConfig } from "../../utils/collectionConfig"; +import { buildImageCollectionConfig } from "../../utils/imageCollectionConfig"; const fields = { filename: "filename", @@ -7,43 +7,39 @@ const fields = { filesize: "filesize", } as const satisfies Record; -export const PostsThumbnails = buildCollectionConfig( - Collections.PostsThumbnails, - { +export const PostsThumbnails = buildImageCollectionConfig({ + slug: Collections.PostsThumbnails, + labels: { singular: "Post Thumbnail", plural: "Post Thumbnails", }, - ({ uploadDir }) => ({ - defaultSort: fields.filename, - admin: { - useAsTitle: fields.filename, - disableDuplicate: true, - group: CollectionGroups.Media, - }, - upload: { - staticDir: uploadDir, - mimeTypes: ["image/*"], - imageSizes: [ - { - name: "og", - height: 750, - width: 1125, - formatOptions: { - format: "jpg", - options: { progressive: true, mozjpeg: true, compressionLevel: 9, quality: 80 }, - }, + defaultSort: fields.filename, + admin: { + useAsTitle: fields.filename, + disableDuplicate: true, + group: CollectionGroups.Media, + }, + upload: { + 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/Recorders/Recorders.ts b/src/collections/Recorders/Recorders.ts index 88ca1d8..8e97dc2 100644 --- a/src/collections/Recorders/Recorders.ts +++ b/src/collections/Recorders/Recorders.ts @@ -3,7 +3,7 @@ import { mustBeAdmin } from "../../accesses/mustBeAdmin"; import { QuickFilters } from "../../components/QuickFilters"; import { CollectionGroups, Collections, RecordersRoles } from "../../constants"; import { imageField } from "../../fields/imageField/imageField"; -import { localizedFields } from "../../fields/translatedFields/translatedFields"; +import { translatedFields } from "../../fields/translatedFields/translatedFields"; import { buildCollectionConfig } from "../../utils/collectionConfig"; import { importFromStrapi } from "./endpoints/importFromStrapi"; import { beforeLoginMustHaveAtLeastOneRole } from "./hooks/beforeLoginMustHaveAtLeastOneRole"; @@ -18,127 +18,125 @@ const fields = { role: "role", } as const satisfies Record; -export const Recorders = buildCollectionConfig( - Collections.Recorders, - { +export const Recorders = buildCollectionConfig({ + slug: Collections.Recorders, + labels: { singular: "Recorder", plural: "Recorders", }, - () => ({ - defaultSort: fields.username, - admin: { - useAsTitle: fields.username, - description: - "Recorders are contributors of the Accord's Library project. Ask an admin to create a \ + defaultSort: fields.username, + admin: { + useAsTitle: fields.username, + description: + "Recorders are contributors of the Accord's Library project. Ask an admin to create a \ Recorder here to be able to credit them in other collections.", - defaultColumns: [ - fields.username, - fields.avatar, - fields.anonymize, - fields.biographies, - fields.languages, - fields.role, - ], - disableDuplicate: true, - group: CollectionGroups.Meta, - components: { - BeforeListTable: [ - () => - QuickFilters({ - slug: Collections.Recorders, - filterGroups: [ - [ - ...Object.entries(RecordersRoles).map(([key, value]) => ({ - label: value, - filter: { where: { role: { equals: key } } }, - })), - { - label: "∅ Role", - filter: { where: { role: { not_in: Object.keys(RecordersRoles).join(",") } } }, - }, - , - ], - [{ label: "Anonymized", filter: { where: { anonymize: { equals: true } } } }], - ], - }), - ], - }, - }, - auth: true, - access: { - unlock: mustBeAdmin, - update: mustBeAdminOrSelf, - delete: mustBeAdmin, - create: mustBeAdmin, - }, - hooks: { - beforeLogin: [beforeLoginMustHaveAtLeastOneRole], - }, - endpoints: [importFromStrapi], - 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: Collections.RecordersThumbnails, - admin: { width: "66%" }, - }), - ], - }, - { - name: fields.languages, - type: "relationship", - relationTo: Collections.Languages, - hasMany: true, - admin: { - allowCreate: false, - description: "List of language(s) that this recorder is familiar with", - }, - }, - 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.role, - type: "select", - access: { - update: mustBeAdmin, - create: mustBeAdmin, - }, - hasMany: true, - options: Object.entries(RecordersRoles).map(([value, label]) => ({ - label, - value, - })), - admin: { position: "sidebar" }, - }, - { - 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", - }, - }, + defaultColumns: [ + fields.username, + fields.avatar, + fields.anonymize, + fields.biographies, + fields.languages, + fields.role, ], - }) -); + disableDuplicate: true, + group: CollectionGroups.Meta, + components: { + BeforeListTable: [ + () => + QuickFilters({ + slug: Collections.Recorders, + filterGroups: [ + [ + ...Object.entries(RecordersRoles).map(([key, value]) => ({ + label: value, + filter: { where: { role: { equals: key } } }, + })), + { + label: "∅ Role", + filter: { where: { role: { not_in: Object.keys(RecordersRoles).join(",") } } }, + }, + , + ], + [{ label: "Anonymized", filter: { where: { anonymize: { equals: true } } } }], + ], + }), + ], + }, + }, + auth: true, + access: { + unlock: mustBeAdmin, + update: mustBeAdminOrSelf, + delete: mustBeAdmin, + create: mustBeAdmin, + }, + hooks: { + beforeLogin: [beforeLoginMustHaveAtLeastOneRole], + }, + endpoints: [importFromStrapi], + 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: Collections.RecordersThumbnails, + admin: { width: "66%" }, + }), + ], + }, + { + name: fields.languages, + type: "relationship", + relationTo: Collections.Languages, + hasMany: true, + admin: { + allowCreate: false, + description: "List of language(s) that this recorder is familiar with", + }, + }, + translatedFields({ + 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.role, + type: "select", + access: { + update: mustBeAdmin, + create: mustBeAdmin, + }, + hasMany: true, + options: Object.entries(RecordersRoles).map(([value, label]) => ({ + label, + value, + })), + admin: { position: "sidebar" }, + }, + { + 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/RecordersThumbnails/RecordersThumbnails.ts b/src/collections/RecordersThumbnails/RecordersThumbnails.ts index 49221d0..c983665 100644 --- a/src/collections/RecordersThumbnails/RecordersThumbnails.ts +++ b/src/collections/RecordersThumbnails/RecordersThumbnails.ts @@ -1,6 +1,6 @@ import { CollectionGroups, Collections } from "../../constants"; import { backPropagationField } from "../../fields/backPropagationField/backPropagationField"; -import { buildCollectionConfig } from "../../utils/collectionConfig"; +import { buildImageCollectionConfig } from "../../utils/imageCollectionConfig"; const fields = { filename: "filename", @@ -9,51 +9,46 @@ const fields = { recorder: "recorder", } as const satisfies Record; -export const RecordersThumbnails = buildCollectionConfig( - Collections.RecordersThumbnails, - { +export const RecordersThumbnails = buildImageCollectionConfig({ + slug: Collections.RecordersThumbnails, + labels: { singular: "Recorders Thumbnail", plural: "Recorders Thumbnails", }, - ({ uploadDir }) => ({ - defaultSort: fields.filename, - admin: { - useAsTitle: fields.filename, - disableDuplicate: true, - group: CollectionGroups.Media, - }, - upload: { - staticDir: uploadDir, - adminThumbnail: "small", - mimeTypes: ["image/*"], - imageSizes: [ - { - name: "og", - height: 256, - width: 256, - formatOptions: { - format: "jpg", - options: { progressive: true, mozjpeg: true, compressionLevel: 9, quality: 80 }, - }, + defaultSort: fields.filename, + admin: { + useAsTitle: fields.filename, + disableDuplicate: true, + group: CollectionGroups.Media, + }, + upload: { + 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: [ - backPropagationField({ - name: fields.recorder, - hasMany: false, - relationTo: Collections.Recorders, - where: (id) => ({ avatar: { equals: id } }), - }), + }, ], - }) -); + }, + fields: [ + backPropagationField({ + name: fields.recorder, + hasMany: false, + relationTo: Collections.Recorders, + where: ({ id }) => ({ avatar: { equals: id } }), + }), + ], +}); diff --git a/src/collections/Videos/Videos.ts b/src/collections/Videos/Videos.ts index f19f0d5..7800ff7 100644 --- a/src/collections/Videos/Videos.ts +++ b/src/collections/Videos/Videos.ts @@ -17,87 +17,85 @@ const fields = { channel: "channel", } as const satisfies Record; -export const Videos: CollectionConfig = buildCollectionConfig( - Collections.Videos, - { +export const Videos: CollectionConfig = buildCollectionConfig({ + slug: Collections.Videos, + labels: { singular: "Video", plural: "Videos", }, - () => ({ - defaultSort: fields.uid, - admin: { - useAsTitle: fields.title, - defaultColumns: [ - fields.uid, - fields.title, - fields.source, - fields.gone, - fields.liveChat, - fields.publishedDate, - fields.views, - fields.likes, - fields.channel, - ], - group: CollectionGroups.Media, - disableDuplicate: true, - }, - access: { - create: mustBeAdmin, - delete: mustBeAdmin, - }, - endpoints: [importFromStrapi], - timestamps: false, - fields: [ - { - type: "row", - fields: [ - { name: fields.uid, type: "text", required: true, unique: true, admin: { width: "33%" } }, - { - name: fields.gone, - type: "checkbox", - defaultValue: false, - required: true, - admin: { - description: - "Is the video no longer available (deleted, privatized, unlisted, blocked...)", - width: "33%", - }, - }, - { - name: fields.source, - type: "select", - required: true, - options: Object.entries(VideoSources).map(([value, label]) => ({ label, value })), - admin: { width: "33%" }, - }, - ], - }, - - { name: fields.title, type: "text", required: true }, - { name: fields.description, type: "textarea" }, - { - type: "row", - fields: [ - { name: fields.likes, type: "number", admin: { width: "50%" } }, - { name: fields.views, type: "number", admin: { width: "50%" } }, - ], - }, - { - name: fields.publishedDate, - type: "date", - admin: { - date: { pickerAppearance: "dayOnly", displayFormat: "yyyy-MM-dd" }, - position: "sidebar", - }, - required: true, - }, - { - name: fields.channel, - type: "relationship", - relationTo: Collections.VideosChannels, - required: true, - admin: { position: "sidebar" }, - }, + defaultSort: fields.uid, + admin: { + useAsTitle: fields.title, + defaultColumns: [ + fields.uid, + fields.title, + fields.source, + fields.gone, + fields.liveChat, + fields.publishedDate, + fields.views, + fields.likes, + fields.channel, ], - }) -); + group: CollectionGroups.Media, + disableDuplicate: true, + }, + access: { + create: mustBeAdmin, + delete: mustBeAdmin, + }, + endpoints: [importFromStrapi], + timestamps: false, + fields: [ + { + type: "row", + fields: [ + { name: fields.uid, type: "text", required: true, unique: true, admin: { width: "33%" } }, + { + name: fields.gone, + type: "checkbox", + defaultValue: false, + required: true, + admin: { + description: + "Is the video no longer available (deleted, privatized, unlisted, blocked...)", + width: "33%", + }, + }, + { + name: fields.source, + type: "select", + required: true, + options: Object.entries(VideoSources).map(([value, label]) => ({ label, value })), + admin: { width: "33%" }, + }, + ], + }, + + { name: fields.title, type: "text", required: true }, + { name: fields.description, type: "textarea" }, + { + type: "row", + fields: [ + { name: fields.likes, type: "number", admin: { width: "50%" } }, + { name: fields.views, type: "number", admin: { width: "50%" } }, + ], + }, + { + name: fields.publishedDate, + type: "date", + admin: { + date: { pickerAppearance: "dayOnly", displayFormat: "yyyy-MM-dd" }, + position: "sidebar", + }, + required: true, + }, + { + name: fields.channel, + type: "relationship", + relationTo: Collections.VideosChannels, + required: true, + admin: { position: "sidebar" }, + }, + ], +}); diff --git a/src/collections/VideosChannels/VideosChannels.ts b/src/collections/VideosChannels/VideosChannels.ts index 56d01d1..471dc2a 100644 --- a/src/collections/VideosChannels/VideosChannels.ts +++ b/src/collections/VideosChannels/VideosChannels.ts @@ -11,35 +11,33 @@ const fields = { videos: "videos", } as const satisfies Record; -export const VideosChannels: CollectionConfig = buildCollectionConfig( - Collections.VideosChannels, - { +export const VideosChannels: CollectionConfig = buildCollectionConfig({ + slug: Collections.VideosChannels, + labels: { singular: "Videos Channel", plural: "Videos Channels", }, - () => ({ - defaultSort: fields.title, - admin: { - useAsTitle: fields.title, - defaultColumns: [fields.uid, fields.title, fields.subscribers, fields.videos], - group: CollectionGroups.Media, - disableDuplicate: true, + defaultSort: fields.title, + admin: { + useAsTitle: fields.title, + defaultColumns: [fields.uid, fields.title, fields.subscribers, fields.videos], + group: CollectionGroups.Media, + disableDuplicate: true, + }, + access: { + create: mustBeAdmin, + delete: mustBeAdmin, + }, + endpoints: [importFromStrapi], + timestamps: false, + fields: [ + { name: fields.uid, type: "text", required: true, unique: true }, + { + type: "row", + fields: [ + { name: fields.title, type: "text", required: true }, + { name: fields.subscribers, type: "number" }, + ], }, - access: { - create: mustBeAdmin, - delete: mustBeAdmin, - }, - endpoints: [importFromStrapi], - timestamps: false, - fields: [ - { name: fields.uid, type: "text", required: true, unique: true }, - { - type: "row", - fields: [ - { name: fields.title, type: "text", required: true }, - { name: fields.subscribers, type: "number" }, - ], - }, - ], - }) -); + ], +}); diff --git a/src/collections/Weapons/Weapons.ts b/src/collections/Weapons/Weapons.ts index e5c6cfe..601cf23 100644 --- a/src/collections/Weapons/Weapons.ts +++ b/src/collections/Weapons/Weapons.ts @@ -1,7 +1,8 @@ import { CollectionGroups, Collections, KeysTypes } from "../../constants"; import { imageField } from "../../fields/imageField/imageField"; +import { keysField } from "../../fields/keysField/keysField"; import { slugField } from "../../fields/slugField/slugField"; -import { localizedFields } from "../../fields/translatedFields/translatedFields"; +import { translatedFields } from "../../fields/translatedFields/translatedFields"; import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig"; import { AppearanceRowLabel } from "./components/AppearanceRowLabel"; import { importFromStrapi } from "./endpoints/importFromStrapi"; @@ -23,141 +24,135 @@ const fields = { status: "_status", }; -export const Weapons = buildVersionedCollectionConfig( - Collections.Weapons, - { singular: "Weapon", plural: "Weapons" }, - () => ({ - defaultSort: fields.slug, - admin: { - useAsTitle: fields.slug, - defaultColumns: [ - fields.slug, - fields.thumbnail, - fields.group, - fields.type, - fields.appearances, - fields.status, - ], - group: CollectionGroups.Collections, - }, - endpoints: [importFromStrapi], - fields: [ - { - type: "row", - fields: [ - slugField({ name: fields.slug, admin: { width: "50%" } }), - imageField({ - name: fields.thumbnail, - relationTo: Collections.WeaponsThumbnails, - admin: { width: "50%" }, - }), - ], - }, - { - type: "row", - fields: [ - { - name: fields.type, - type: "relationship", - relationTo: Collections.Keys, - required: true, - filterOptions: { type: { equals: KeysTypes.Weapons } }, - admin: { allowCreate: false, width: "50%" }, - }, - { - name: fields.group, - type: "relationship", - relationTo: Collections.WeaponsGroups, - admin: { width: "50%" }, - }, - ], - }, - { - name: fields.appearances, - type: "array", - required: true, - minRows: 1, - admin: { - initCollapsed: true, - components: { - RowLabel: ({ data }) => - AppearanceRowLabel({ keyIds: data[fields.appearancesCategories] ?? [] }), - }, - }, - fields: [ - { - name: fields.appearancesCategories, - type: "relationship", - required: true, - hasMany: true, - relationTo: Collections.Keys, - filterOptions: { type: { equals: KeysTypes.Categories } }, - admin: { allowCreate: false }, - }, - localizedFields({ - name: fields.appearancesTranslations, - required: true, - minRows: 1, - admin: { - useAsTitle: fields.appearancesTranslationsName, - hasSourceLanguage: true, - hasCredits: true, - }, - fields: [ - { - type: "row", - fields: [ - { - name: fields.appearancesTranslationsName, - type: "text", - required: true, - admin: { width: "50%" }, - }, - { - name: fields.appearancesTranslationsDescription, - type: "textarea", - admin: { width: "50%" }, - }, - ], - }, - { - type: "row", - fields: [ - { - name: fields.appearancesTranslationsLevel1, - label: "Level 1", - type: "textarea", - admin: { width: "50%" }, - }, - { - name: fields.appearancesTranslationsLevel2, - label: "Level 2", - type: "textarea", - admin: { width: "50%" }, - }, - ], - }, - { - type: "row", - fields: [ - { - name: fields.appearancesTranslationsLevel3, - label: "Level 3", - type: "textarea", - admin: { width: "50%" }, - }, - { - name: fields.appearancesTranslationsLevel4, - label: "Level 4", - type: "textarea", - admin: { width: "50%" }, - }, - ], - }, - ], - }), - ], - }, +export const Weapons = buildVersionedCollectionConfig({ + slug: Collections.Weapons, + labels: { singular: "Weapon", plural: "Weapons" }, + defaultSort: fields.slug, + admin: { + useAsTitle: fields.slug, + defaultColumns: [ + fields.slug, + fields.thumbnail, + fields.group, + fields.type, + fields.appearances, + fields.status, ], - }) -); + group: CollectionGroups.Collections, + }, + endpoints: [importFromStrapi], + fields: [ + { + type: "row", + fields: [ + slugField({ name: fields.slug, admin: { width: "50%" } }), + imageField({ + name: fields.thumbnail, + relationTo: Collections.WeaponsThumbnails, + admin: { width: "50%" }, + }), + ], + }, + { + type: "row", + fields: [ + keysField({ + name: fields.type, + relationTo: KeysTypes.Weapons, + required: true, + admin: { allowCreate: false, width: "50%" }, + }), + { + name: fields.group, + type: "relationship", + relationTo: Collections.WeaponsGroups, + admin: { width: "50%" }, + }, + ], + }, + { + name: fields.appearances, + type: "array", + required: true, + minRows: 1, + admin: { + initCollapsed: true, + components: { + RowLabel: ({ data }) => + AppearanceRowLabel({ keyIds: data[fields.appearancesCategories] ?? [] }), + }, + }, + fields: [ + keysField({ + name: fields.appearancesCategories, + required: true, + hasMany: true, + relationTo: KeysTypes.Categories, + admin: { allowCreate: false }, + }), + translatedFields({ + name: fields.appearancesTranslations, + required: true, + minRows: 1, + admin: { + useAsTitle: fields.appearancesTranslationsName, + hasSourceLanguage: true, + hasCredits: true, + }, + fields: [ + { + type: "row", + fields: [ + { + name: fields.appearancesTranslationsName, + type: "text", + required: true, + admin: { width: "50%" }, + }, + { + name: fields.appearancesTranslationsDescription, + type: "textarea", + admin: { width: "50%" }, + }, + ], + }, + { + type: "row", + fields: [ + { + name: fields.appearancesTranslationsLevel1, + label: "Level 1", + type: "textarea", + admin: { width: "50%" }, + }, + { + name: fields.appearancesTranslationsLevel2, + label: "Level 2", + type: "textarea", + admin: { width: "50%" }, + }, + ], + }, + { + type: "row", + fields: [ + { + name: fields.appearancesTranslationsLevel3, + label: "Level 3", + type: "textarea", + admin: { width: "50%" }, + }, + { + name: fields.appearancesTranslationsLevel4, + label: "Level 4", + type: "textarea", + admin: { width: "50%" }, + }, + ], + }, + ], + }), + ], + }, + ], +}); diff --git a/src/collections/WeaponsGroups/WeaponsGroups.ts b/src/collections/WeaponsGroups/WeaponsGroups.ts index c0b5fbc..2ccb8df 100644 --- a/src/collections/WeaponsGroups/WeaponsGroups.ts +++ b/src/collections/WeaponsGroups/WeaponsGroups.ts @@ -1,7 +1,7 @@ import { CollectionGroups, Collections } from "../../constants"; import { backPropagationField } from "../../fields/backPropagationField/backPropagationField"; import { slugField } from "../../fields/slugField/slugField"; -import { localizedFields } from "../../fields/translatedFields/translatedFields"; +import { translatedFields } from "../../fields/translatedFields/translatedFields"; import { buildCollectionConfig } from "../../utils/collectionConfig"; const fields = { @@ -12,30 +12,28 @@ const fields = { weapons: "weapons", }; -export const WeaponsGroups = buildCollectionConfig( - Collections.WeaponsGroups, - { singular: "Weapons Group", plural: "Weapon Groups" }, - () => ({ - defaultSort: fields.slug, - admin: { - useAsTitle: fields.slug, - defaultColumns: [fields.slug, fields.translations, fields.weapons, fields.subgroupOf], - group: CollectionGroups.Collections, - }, - timestamps: false, - fields: [ - slugField({ name: fields.slug }), - localizedFields({ - name: fields.translations, - admin: { useAsTitle: fields.translationsName }, - fields: [{ name: fields.translationsName, type: "text", required: true }], - }), - backPropagationField({ - name: fields.weapons, - relationTo: Collections.Weapons, - hasMany: true, - where: (id) => ({ group: { equals: id } }), - }), - ], - }) -); +export const WeaponsGroups = buildCollectionConfig({ + slug: Collections.WeaponsGroups, + labels: { singular: "Weapons Group", plural: "Weapon Groups" }, + defaultSort: fields.slug, + admin: { + useAsTitle: fields.slug, + defaultColumns: [fields.slug, fields.translations, fields.weapons, fields.subgroupOf], + group: CollectionGroups.Collections, + }, + timestamps: false, + fields: [ + slugField({ name: fields.slug }), + translatedFields({ + name: fields.translations, + admin: { useAsTitle: fields.translationsName }, + fields: [{ name: fields.translationsName, type: "text", required: true }], + }), + backPropagationField({ + name: fields.weapons, + relationTo: Collections.Weapons, + hasMany: true, + where: ({ id }) => ({ group: { equals: id } }), + }), + ], +}); diff --git a/src/collections/WeaponsThumbnails/WeaponsThumbnails.ts b/src/collections/WeaponsThumbnails/WeaponsThumbnails.ts index cef4398..57b61a6 100644 --- a/src/collections/WeaponsThumbnails/WeaponsThumbnails.ts +++ b/src/collections/WeaponsThumbnails/WeaponsThumbnails.ts @@ -1,5 +1,5 @@ import { CollectionGroups, Collections } from "../../constants"; -import { buildCollectionConfig } from "../../utils/collectionConfig"; +import { buildImageCollectionConfig } from "../../utils/imageCollectionConfig"; const fields = { filename: "filename", @@ -7,44 +7,60 @@ const fields = { filesize: "filesize", } as const satisfies Record; -export const WeaponsThumbnails = buildCollectionConfig( - Collections.WeaponsThumbnails, - { +export const WeaponsThumbnails = buildImageCollectionConfig({ + slug: Collections.WeaponsThumbnails, + labels: { singular: "Weapons Thumbnail", plural: "Weapons Thumbnails", }, - ({ uploadDir }) => ({ - defaultSort: fields.filename, - admin: { - useAsTitle: fields.filename, - disableDuplicate: true, - group: CollectionGroups.Media, - }, - upload: { - staticDir: uploadDir, - adminThumbnail: "small", - mimeTypes: ["image/*"], - imageSizes: [ - { - name: "og", - height: 256, - width: 256, - formatOptions: { - format: "jpg", - options: { progressive: true, mozjpeg: true, compressionLevel: 9, quality: 80 }, - }, + defaultSort: fields.filename, + admin: { + useAsTitle: fields.filename, + disableDuplicate: true, + group: CollectionGroups.Media, + }, + upload: { + imageSizes: [ + { + name: "og", + height: 512, + width: 512, + fit: "inside", + 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: 256, + width: 256, + fit: "contain", + background: { r: 0, g: 0, b: 0, alpha: 0 }, + formatOptions: { + format: "webp", + options: { effort: 6, quality: 70, alphaQuality: 70 }, }, - ], - }, - fields: [], - }) -); + }, + { + name: "medium", + height: 1024, + width: 1024, + fit: "contain", + background: { r: 0, g: 0, b: 0, alpha: 0 }, + formatOptions: { + format: "webp", + options: { effort: 6, quality: 80, alphaQuality: 80 }, + }, + }, + { + name: "max", + formatOptions: { + format: "webp", + options: { effort: 6, quality: 80, alphaQuality: 80 }, + }, + }, + ], + }, + fields: [], +}); diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index ca3f916..9a61570 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { styled } from "styled-components"; export const Icon = styled.div` @@ -9,4 +8,8 @@ export const Icon = styled.div` mask-size: contain; mask-repeat: no-repeat; mask-position: center; + -webkit-mask: url("/public/accords.svg"); + -webkit-mask-size: contain; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; `; diff --git a/src/components/Logo.tsx b/src/components/Logo.tsx index d193a3f..0da1855 100644 --- a/src/components/Logo.tsx +++ b/src/components/Logo.tsx @@ -1,5 +1,5 @@ -import React from "react"; import "@fontsource/vollkorn/700.css"; +import React from "react"; import { styled } from "styled-components"; export const Logo = (): JSX.Element => ( @@ -34,4 +34,8 @@ const Icon = styled.div` mask-size: contain; mask-repeat: no-repeat; mask-position: center; + -webkit-mask: url("/public/accords.svg"); + -webkit-mask-size: contain; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; `; diff --git a/src/constants.ts b/src/constants.ts index aee9400..d164a4e 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -10,6 +10,8 @@ export enum Collections { Languages = "languages", LibraryItems = "library-items", LibraryItemsThumbnails = "library-items-thumbnails", + LibraryItemsScans = "library-items-scans", + LibraryItemsGallery = "library-items-gallery", Posts = "posts", PostsThumbnails = "posts-thumbnails", Recorders = "recorders", @@ -87,4 +89,5 @@ export enum CollectionStatus { export enum VideoSources { YouTube = "YouTube", NicoNico = "NicoNico", + Tumblr = "Tumblr", } diff --git a/src/endpoints/createStrapiImportEndpoint.ts b/src/endpoints/createStrapiImportEndpoint.ts index 915606d..5decdcc 100644 --- a/src/endpoints/createStrapiImportEndpoint.ts +++ b/src/endpoints/createStrapiImportEndpoint.ts @@ -39,7 +39,7 @@ type Params = { payload: { collection: string; import?: (strapiObject: any, user: any) => Promise; - convert?: (strapiObject: any) => PayloadCreateData; + convert?: (strapiObject: any, user: any) => PayloadCreateData; }; }; @@ -60,7 +60,7 @@ export const importStrapiEntries = async ({ } else if (isDefined(payloadParams.convert)) { await payload.create({ collection: payloadParams.collection, - data: payloadParams.convert(attributes), + data: payloadParams.convert(attributes, user), user, }); } else { diff --git a/src/fields/backPropagationField/backPropagationField.ts b/src/fields/backPropagationField/backPropagationField.ts index 5d596e9..139355d 100644 --- a/src/fields/backPropagationField/backPropagationField.ts +++ b/src/fields/backPropagationField/backPropagationField.ts @@ -4,18 +4,20 @@ import { RelationshipField, Where } from "payload/types"; import { isNotEmpty } from "../../utils/asserts"; type BackPropagationField = FieldBase & { - where: (id: string) => Where; + where: (data: any) => Where; relationTo: string; - hasMany: boolean; + hasMany?: boolean; }; export const backPropagationField = ({ admin, hooks: { beforeChange = [], afterRead = [], ...otherHooks } = {}, where, + hasMany = false, ...params }: BackPropagationField): RelationshipField => ({ ...params, type: "relationship", + hasMany: hasMany, admin: { ...admin, readOnly: true }, hooks: { ...otherHooks, @@ -31,17 +33,17 @@ export const backPropagationField = ({ if (isNotEmpty(data.id)) { const result = await payload.find({ collection: params.relationTo, - where: where(data.id), + where: where(data), limit: 100, depth: 0, }); - if (params.hasMany) { + if (hasMany) { return result.docs.map((doc) => doc.id); } else { return result.docs[0].id; } } - return params.hasMany ? [] : undefined; + return hasMany ? [] : undefined; }, ], }, diff --git a/src/fields/imageField/Cell.tsx b/src/fields/imageField/Cell.tsx index 8db28f2..3d90376 100644 --- a/src/fields/imageField/Cell.tsx +++ b/src/fields/imageField/Cell.tsx @@ -1,9 +1,23 @@ import { Props } from "payload/components/views/Cell"; -import { useState, useEffect } from "react"; -import React from "react"; +import React, { useEffect, useMemo, useState } from "react"; +import { Link } from "react-router-dom"; +import { styled } from "styled-components"; import { isUndefined } from "../../utils/asserts"; -export const Cell = ({ cellData, field }: Props): JSX.Element => { +const Image = styled.img` + height: 3rem; + width: 3rem; + object-fit: contain; + transition: 0.2s transform; + transition-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1); + position: absolute; + transform: translateY(-50%) scale(1); + &:hover { + transform: translateY(-50%) scale(3); + } +`; + +export const Cell = ({ cellData, field, rowData, collection }: Props): JSX.Element => { const [imageURL, setImageURL] = useState(); useEffect(() => { const fetchUrl = async () => { @@ -11,18 +25,14 @@ export const Cell = ({ cellData, field }: Props): JSX.Element => { if (typeof cellData !== "string") return; if (field.type !== "upload") return; const result = await (await fetch(`/api/${field.relationTo}/${cellData}`)).json(); - setImageURL(result.url); + setImageURL(result.sizes.thumb.url); }; fetchUrl(); }, [cellData]); - - return ( - <> - {imageURL ? ( - - ) : ( - "" - )} - + const link = useMemo( + () => `/admin/collections/${collection.slug}/${rowData.id}`, + [collection.slug, rowData.id] ); + + return {imageURL ? : ""}; }; diff --git a/src/fields/keysField/keysField.ts b/src/fields/keysField/keysField.ts new file mode 100644 index 0000000..380b33e --- /dev/null +++ b/src/fields/keysField/keysField.ts @@ -0,0 +1,23 @@ +import { FieldBase, RelationshipField } from "payload/dist/fields/config/types"; +import { Collections, KeysTypes } from "../../constants"; + +type KeysField = FieldBase & { + relationTo: KeysTypes; + hasMany?: boolean; + admin: RelationshipField["admin"]; +}; + +export const keysField = ({ + relationTo, + hasMany = false, + ...props +}: KeysField): RelationshipField => ({ + ...props, + type: "relationship", + hasMany: hasMany, + relationTo: Collections.Keys, + filterOptions: { type: { equals: getKeysTypesKey(relationTo) } }, +}); + +const getKeysTypesKey = (keyType: KeysTypes): string => + Object.entries(KeysTypes).find(([, value]) => value === keyType)[0]; diff --git a/src/fields/translatedFields/translatedFields.ts b/src/fields/translatedFields/translatedFields.ts index 7410fb3..c6821d8 100644 --- a/src/fields/translatedFields/translatedFields.ts +++ b/src/fields/translatedFields/translatedFields.ts @@ -1,8 +1,7 @@ import { array } from "payload/dist/fields/validations"; import { ArrayField, Field } from "payload/types"; import { Collections } from "../../constants"; -import { isDefined, isUndefined } from "../../utils/asserts"; -import { hasDuplicates } from "../../utils/validation"; +import { hasDuplicates, isDefined, isUndefined } from "../../utils/asserts"; import { Cell } from "./Cell"; import { RowLabel } from "./RowLabel"; @@ -14,7 +13,7 @@ const fieldsNames = { proofreaders: "proofreaders", } as const satisfies Record; -type LocalizedFieldsProps = Omit & { +type LocalizedFieldsProps = Omit & { admin?: ArrayField["admin"] & { useAsTitle?: string; hasSourceLanguage?: boolean; @@ -43,7 +42,8 @@ const creditFields: Field = { type: "row", admin: { condition: (_, siblingData) => - isDefined(siblingData.language) && isDefined(siblingData.sourceLanguage), + isDefined(siblingData[fieldsNames.language]) && + isDefined(siblingData[fieldsNames.sourceLanguage]), }, fields: [ { @@ -52,18 +52,28 @@ const creditFields: Field = { type: "relationship", relationTo: "recorders", hasMany: true, + hooks: { + beforeChange: [ + ({ siblingData }) => { + if (siblingData[fieldsNames.language] !== siblingData[fieldsNames.sourceLanguage]) { + delete siblingData[fieldsNames.transcribers]; + } + }, + ], + }, admin: { condition: (_, siblingData) => siblingData.language === siblingData.sourceLanguage, width: "50%", }, validate: (count, { siblingData }) => { - if (siblingData.language !== siblingData.sourceLanguage) { + if (siblingData[fieldsNames.language] !== siblingData[fieldsNames.sourceLanguage]) { return true; } if (isDefined(count) && count.length > 0) { return true; } - return "This field is required when the language is the same as the source language."; + return `This field is required when the ${fieldsNames.language} \ + is the same as the ${fieldsNames.sourceLanguage}.`; }, }, { @@ -72,18 +82,29 @@ const creditFields: Field = { type: "relationship", relationTo: "recorders", hasMany: true, + hooks: { + beforeChange: [ + ({ siblingData }) => { + if (siblingData[fieldsNames.language] === siblingData[fieldsNames.sourceLanguage]) { + delete siblingData[fieldsNames.translators]; + } + }, + ], + }, admin: { - condition: (_, siblingData) => siblingData.language !== siblingData.sourceLanguage, + condition: (_, siblingData) => + siblingData[fieldsNames.language] !== siblingData[fieldsNames.sourceLanguage], width: "50%", }, validate: (count, { siblingData }) => { - if (siblingData.language === siblingData.sourceLanguage) { + if (siblingData[fieldsNames.language] === siblingData[fieldsNames.sourceLanguage]) { return true; } if (isDefined(count) && count.length > 0) { return true; } - return "This field is required when the language is different from the source language."; + return `This field is required when the ${fieldsNames.language} \ + is different from the ${fieldsNames.sourceLanguage}.`; }, }, { @@ -97,8 +118,9 @@ const creditFields: Field = { ], }; -export const localizedFields = ({ +export const translatedFields = ({ fields, + validate, admin: { useAsTitle, hasSourceLanguage, hasCredits, ...admin } = {}, ...otherProps }: LocalizedFieldsProps): ArrayField => ({ @@ -111,13 +133,13 @@ export const localizedFields = ({ Cell({ cellData: cellData?.map((row) => ({ - language: row.language, + language: row[fieldsNames.language], title: isDefined(useAsTitle) ? row[useAsTitle] : undefined, })) ?? [], }), RowLabel: ({ data }) => RowLabel({ - language: data.language, + language: data[fieldsNames.language], title: isDefined(useAsTitle) ? data[useAsTitle] : undefined, }), }, @@ -127,11 +149,16 @@ export const localizedFields = ({ const defaultValidation = array(value, options); if (defaultValidation !== true) return defaultValidation; + if (isDefined(validate)) { + const propsValidation = validate(value, options); + if (propsValidation !== true) return propsValidation; + } + const data = options.data[otherProps.name] as ArrayData; if (isUndefined(data)) return true; if (typeof data === "number") return true; - const languages = data.map((biography) => biography.language); + const languages = data.map((rows) => rows[fieldsNames.language]); if (hasDuplicates(languages)) { return `There cannot be multiple ${otherProps.name} with the same ${fieldsNames.language}`; } diff --git a/src/payload.config.ts b/src/payload.config.ts index e4b32cc..9652f6f 100644 --- a/src/payload.config.ts +++ b/src/payload.config.ts @@ -10,6 +10,8 @@ import { Files } from "./collections/Files/Files"; import { Keys } from "./collections/Keys/Keys"; import { Languages } from "./collections/Languages/Languages"; import { LibraryItems } from "./collections/LibraryItems/LibraryItems"; +import { LibraryItemsGallery } from "./collections/LibraryItemsGallery/LibraryItemsGallery"; +import { LibraryItemsScans } from "./collections/LibraryItemsScans/LibraryItemsScans"; import { LibraryItemsThumbnails } from "./collections/LibraryItemsThumbnails/LibraryItemsThumbnails"; import { Posts } from "./collections/Posts/Posts"; import { PostsThumbnails } from "./collections/PostsThumbnails/PostsThumbnails"; @@ -48,6 +50,8 @@ export default buildConfig({ WeaponsThumbnails, ContentsThumbnails, LibraryItemsThumbnails, + LibraryItemsScans, + LibraryItemsGallery, RecordersThumbnails, PostsThumbnails, Files, diff --git a/src/types/collections.ts b/src/types/collections.ts index eb2ae20..8d5773c 100644 --- a/src/types/collections.ts +++ b/src/types/collections.ts @@ -36,6 +36,8 @@ export interface Config { "weapons-thumbnails": WeaponsThumbnail; "contents-thumbnails": ContentsThumbnail; "library-items-thumbnails": LibraryItemThumbnail; + "library-items-scans": LibraryItemScans; + "library-items-gallery": LibraryItemGallery; "recorders-thumbnails": RecordersThumbnail; "posts-thumbnails": PostThumbnail; files: File; @@ -50,6 +52,7 @@ export interface Config { } export interface LibraryItem { id: string; + itemType?: "Textual" | "Audio" | "Video" | "Game" | "Other"; slug: string; thumbnail?: string | LibraryItemThumbnail; pretitle?: string; @@ -59,32 +62,72 @@ export interface LibraryItem { primary: boolean; digital: boolean; downloadable: boolean; + gallery?: { + image?: string | LibraryItemGallery; + id?: string; + }[]; scans?: { cover?: { - front?: string | LibraryItemThumbnail; - spine?: string | LibraryItemThumbnail; - back?: string | LibraryItemThumbnail; + front?: string | LibraryItemScans; + spine?: string | LibraryItemScans; + back?: string | LibraryItemScans; + insideFront?: string | LibraryItemScans; + flapFront?: string | LibraryItemScans; + flapBack?: string | LibraryItemScans; + insideFlapFront?: string | LibraryItemScans; + insideFlapBack?: string | LibraryItemScans; id?: string; }[]; dustjacket?: { - front?: string | LibraryItemThumbnail; - spine?: string | LibraryItemThumbnail; - back?: string | LibraryItemThumbnail; + front?: string | LibraryItemScans; + spine?: string | LibraryItemScans; + back?: string | LibraryItemScans; + insideFront?: string | LibraryItemScans; + insideSpine?: string | LibraryItemScans; + insideBack?: string | LibraryItemScans; + flapFront?: string | LibraryItemScans; + flapBack?: string | LibraryItemScans; + insideFlapFront?: string | LibraryItemScans; + insideFlapBack?: string | LibraryItemScans; id?: string; }[]; obi?: { - front?: string | LibraryItemThumbnail; - spine?: string | LibraryItemThumbnail; - back?: string | LibraryItemThumbnail; + front?: string | LibraryItemScans; + spine?: string | LibraryItemScans; + back?: string | LibraryItemScans; + insideFront?: string | LibraryItemScans; + insideSpine?: string | LibraryItemScans; + insideBack?: string | LibraryItemScans; + flapFront?: string | LibraryItemScans; + flapBack?: string | LibraryItemScans; + insideFlapFront?: string | LibraryItemScans; + insideFlapBack?: string | LibraryItemScans; id?: string; }[]; pages?: { page: number; - image: string | LibraryItemThumbnail; + image: string | LibraryItemScans; id?: string; }[]; id?: string; }[]; + textual?: { + subtype?: string[] | Key[]; + languages?: string[] | Language[]; + pageCount?: number; + bindingType?: "Paperback" | "Hardcover"; + pageOrder?: "LeftToRight" | "RightToLeft"; + }; + audio?: { + audioSubtype?: string[] | Key[]; + }; + releaseDate?: string; + categories?: string[] | Key[]; + translations?: { + language: string | Language; + description: string; + id?: string; + }[]; size?: { width: number; height: number; @@ -96,41 +139,10 @@ export interface LibraryItem { currency: string | Currency; id?: string; }[]; - 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"; - }; - audio?: { - audioSubtype?: - | { - value: string; - relationTo: "keys"; - }[] - | { - value: Key; - relationTo: "keys"; - }[]; - }; + urls?: { + url: string; + id?: string; + }[]; contents?: { content: string | Content; pageStart?: number; @@ -140,7 +152,6 @@ export interface LibraryItem { note?: string; id?: string; }[]; - releaseDate?: string; updatedBy: string | Recorder; updatedAt: string; createdAt: string; @@ -157,6 +168,96 @@ export interface LibraryItemThumbnail { width?: number; height?: number; sizes?: { + thumb?: { + url?: string; + width?: number; + height?: number; + mimeType?: string; + filesize?: number; + filename?: string; + }; + og?: { + url?: string; + width?: number; + height?: number; + mimeType?: string; + filesize?: number; + filename?: string; + }; + square?: { + url?: string; + width?: number; + height?: number; + mimeType?: string; + filesize?: number; + filename?: string; + }; + max?: { + url?: string; + width?: number; + height?: number; + mimeType?: string; + filesize?: number; + filename?: string; + }; + }; +} +export interface LibraryItemGallery { + id: string; + updatedAt: string; + createdAt: string; + url?: string; + filename?: string; + mimeType?: string; + filesize?: number; + width?: number; + height?: number; + sizes?: { + thumb?: { + 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; + }; + max?: { + url?: string; + width?: number; + height?: number; + mimeType?: string; + filesize?: number; + filename?: string; + }; + }; +} +export interface LibraryItemScans { + id: string; + updatedAt: string; + createdAt: string; + url?: string; + filename?: string; + mimeType?: string; + filesize?: number; + width?: number; + height?: number; + sizes?: { + thumb?: { + url?: string; + width?: number; + height?: number; + mimeType?: string; + filesize?: number; + filename?: string; + }; og?: { url?: string; width?: number; @@ -183,9 +284,6 @@ export interface LibraryItemThumbnail { }; }; } -export interface Currency { - id: string; -} export interface Key { id: string; name: string; @@ -206,23 +304,15 @@ export interface Language { id: string; name: string; } +export interface Currency { + id: string; +} export interface Content { id: string; slug: string; thumbnail?: string | ContentsThumbnail; - categories?: - | { - value: string; - relationTo: "keys"; - }[] - | { - value: Key; - relationTo: "keys"; - }[]; - type?: { - value: string | Key; - relationTo: "keys"; - }; + categories?: string[] | Key[]; + type?: string | Key; translations: { language: string | Language; sourceLanguage: string | Language; @@ -256,6 +346,14 @@ export interface ContentsThumbnail { width?: number; height?: number; sizes?: { + thumb?: { + url?: string; + width?: number; + height?: number; + mimeType?: string; + filesize?: number; + filename?: string; + }; og?: { url?: string; width?: number; @@ -303,6 +401,14 @@ export interface RecordersThumbnail { width?: number; height?: number; sizes?: { + thumb?: { + url?: string; + width?: number; + height?: number; + mimeType?: string; + filesize?: number; + filename?: string; + }; og?: { url?: string; width?: number; @@ -574,6 +680,14 @@ export interface PostThumbnail { width?: number; height?: number; sizes?: { + thumb?: { + url?: string; + width?: number; + height?: number; + mimeType?: string; + filesize?: number; + filename?: string; + }; og?: { url?: string; width?: number; @@ -600,12 +714,25 @@ export interface ChronologyItem { month?: number; day?: number; }; - events?: { - translations?: { + events: { + source?: + | { + value: string | Content; + relationTo: "contents"; + } + | { + value: string | LibraryItem; + relationTo: "library-items"; + }; + translations: { language: string | Language; + sourceLanguage: string | Language; title?: string; description?: string; notes?: string; + transcribers?: string[] | Recorder[]; + translators?: string[] | Recorder[]; + proofreaders?: string[] | Recorder[]; id?: string; }[]; id?: string; @@ -626,6 +753,7 @@ export interface ChronologyEra { description?: string; id?: string; }[]; + events?: string[] | ChronologyItem[]; updatedAt: string; createdAt: string; } @@ -669,6 +797,14 @@ export interface WeaponsThumbnail { width?: number; height?: number; sizes?: { + thumb?: { + url?: string; + width?: number; + height?: number; + mimeType?: string; + filesize?: number; + filename?: string; + }; og?: { url?: string; width?: number; @@ -685,6 +821,22 @@ export interface WeaponsThumbnail { filesize?: number; filename?: string; }; + medium?: { + url?: string; + width?: number; + height?: number; + mimeType?: string; + filesize?: number; + filename?: string; + }; + max?: { + url?: string; + width?: number; + height?: number; + mimeType?: string; + filesize?: number; + filename?: string; + }; }; } export interface WeaponsGroup { @@ -701,7 +853,7 @@ export interface Video { id: string; uid: string; gone: boolean; - source: "YouTube" | "NicoNico"; + source: "YouTube" | "NicoNico" | "Tumblr"; title: string; description?: string; likes?: number; diff --git a/src/utils/asserts.ts b/src/utils/asserts.ts index 50930d4..cf8314d 100644 --- a/src/utils/asserts.ts +++ b/src/utils/asserts.ts @@ -10,3 +10,17 @@ export const isValidDate = (date: Date): boolean => date instanceof Date && !isN export const isNotEmpty = (value: string | null | undefined): value is string => isDefined(value) && value.trim().length > 0; + +export const isEmpty = (value: string | null | undefined): value is string => + isUndefined(value) || value.trim().length === 0; + +type Span = [number, number]; +export const hasNoIntersection = (a: Span, b: Span): boolean => { + const [aStart, aEnd] = a; + const [bStart, bEnd] = b; + return aEnd < bStart || aStart > bEnd; +}; + +export const hasIntersection = (a: Span, b: Span): boolean => !hasNoIntersection(a, b); + +export const hasDuplicates = (list: T[]): boolean => list.length !== new Set(list).size; diff --git a/src/utils/collectionConfig.ts b/src/utils/collectionConfig.ts index d43a80f..756da4b 100644 --- a/src/utils/collectionConfig.ts +++ b/src/utils/collectionConfig.ts @@ -1,22 +1,12 @@ import { CollectionConfig } from "payload/types"; import { Collections } from "../constants"; -export type BuildCollectionConfig = Omit; - -export type GenerationFunctionProps = { - uploadDir: string; +export type BuildCollectionConfig = Omit & { + slug: Collections; + labels: { singular: string; plural: string }; }; -export const buildCollectionConfig = ( - slug: Collections, - labels: { singular: string; plural: string }, - generationFunction: (props: GenerationFunctionProps) => BuildCollectionConfig -): CollectionConfig => { - const uploadDir = `../uploads/${slug}`; - const config = generationFunction({ uploadDir }); - return { - ...config, - slug, - typescript: { interface: labels.singular }, - }; -}; +export const buildCollectionConfig = (config: BuildCollectionConfig): CollectionConfig => ({ + ...config, + typescript: { interface: config.labels.singular }, +}); diff --git a/src/utils/imageCollectionConfig.ts b/src/utils/imageCollectionConfig.ts new file mode 100644 index 0000000..ee6e2a7 --- /dev/null +++ b/src/utils/imageCollectionConfig.ts @@ -0,0 +1,34 @@ +import { ImageSize } from "payload/dist/uploads/types"; +import { CollectionConfig } from "payload/types"; +import { BuildCollectionConfig, buildCollectionConfig } from "./collectionConfig"; + +type BuildImageCollectionConfig = Omit & { + upload: { imageSizes: ImageSize[] }; +}; + +export const buildImageCollectionConfig = ({ + upload: { imageSizes }, + ...otherConfig +}: BuildImageCollectionConfig): CollectionConfig => + buildCollectionConfig({ + ...otherConfig, + upload: { + staticDir: `../uploads/${otherConfig.slug}`, + mimeTypes: ["image/*"], + adminThumbnail: "thumb", + imageSizes: [ + { + name: "thumb", + height: 128, + width: 128, + fit: "contain", + background: { r: 0, g: 0, b: 0, alpha: 0 }, + formatOptions: { + format: "webp", + options: { effort: 6, quality: 50, alphaQuality: 50 }, + }, + }, + ...imageSizes, + ], + }, + }); diff --git a/src/utils/validation.ts b/src/utils/validation.ts deleted file mode 100644 index 1877248..0000000 --- a/src/utils/validation.ts +++ /dev/null @@ -1 +0,0 @@ -export const hasDuplicates = (list: T[]): boolean => list.length !== new Set(list).size; diff --git a/src/utils/versionedCollectionConfig.ts b/src/utils/versionedCollectionConfig.ts index 2cfa3e5..68771db 100644 --- a/src/utils/versionedCollectionConfig.ts +++ b/src/utils/versionedCollectionConfig.ts @@ -1,10 +1,6 @@ import { CollectionBeforeChangeHook, CollectionConfig, RelationshipField } from "payload/types"; import { Collections } from "../constants"; -import { - BuildCollectionConfig, - GenerationFunctionProps, - buildCollectionConfig, -} from "./collectionConfig"; +import { BuildCollectionConfig } from "./collectionConfig"; const fields = { updatedBy: "updatedBy" }; @@ -23,25 +19,17 @@ const updatedByField = (): RelationshipField => ({ type BuildVersionedCollectionConfig = Omit; -export const buildVersionedCollectionConfig = ( - slug: Collections, - labels: { singular: string; plural: string }, - generationFunction: (props: GenerationFunctionProps) => BuildVersionedCollectionConfig -): CollectionConfig => { - const { - hooks: { beforeChange, ...otherHooks } = {}, - fields, - ...otherParams - } = buildCollectionConfig(slug, labels, generationFunction); - - return { - ...otherParams, - timestamps: true, - versions: { drafts: { autosave: { interval: 2000 } } }, - hooks: { - ...otherHooks, - beforeChange: [...(beforeChange ?? []), beforeChangeUpdatedBy], - }, - fields: [...fields, updatedByField()], - }; -}; +export const buildVersionedCollectionConfig = ({ + hooks: { beforeChange, ...otherHooks } = {}, + fields, + ...otherParams +}: BuildVersionedCollectionConfig): CollectionConfig => ({ + ...otherParams, + timestamps: true, + versions: { drafts: { autosave: { interval: 2000 } } }, + hooks: { + ...otherHooks, + beforeChange: [...(beforeChange ?? []), beforeChangeUpdatedBy], + }, + fields: [...fields, updatedByField()], +});