Lots of things, again

This commit is contained in:
DrMint 2023-08-14 14:03:08 +02:00
parent d9f84f59a8
commit f56ba4675f
43 changed files with 2505 additions and 1907 deletions

70
package-lock.json generated
View File

@ -12,15 +12,10 @@
"@fontsource/vollkorn": "^5.0.8", "@fontsource/vollkorn": "^5.0.8",
"clean-deep": "^3.4.0", "clean-deep": "^3.4.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"language-tags": "^1.0.8", "language-tags": "^1.0.8",
"luxon": "^3.4.0", "luxon": "^3.4.0",
"payload": "^1.13.3", "payload": "^1.13.3",
"qs": "^6.11.2", "styled-components": "^6.0.7"
"slugify": "^1.6.6",
"styled-components": "^6.0.7",
"unset-value": "^2.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/dotenv": "^8.2.0", "@types/dotenv": "^8.2.0",
@ -6887,6 +6882,7 @@
"version": "16.3.1", "version": "16.3.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
"dev": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@ -7565,17 +7561,6 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" "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": { "node_modules/github-from-package": {
"version": "0.0.0", "version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "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" "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": { "node_modules/he": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "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", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.6.tgz",
"integrity": "sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA==" "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": { "node_modules/smart-buffer": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
@ -13540,26 +13494,6 @@
"node": ">= 0.8" "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": { "node_modules/untildify": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",

View File

@ -24,15 +24,10 @@
"@fontsource/vollkorn": "^5.0.8", "@fontsource/vollkorn": "^5.0.8",
"clean-deep": "^3.4.0", "clean-deep": "^3.4.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"language-tags": "^1.0.8", "language-tags": "^1.0.8",
"luxon": "^3.4.0", "luxon": "^3.4.0",
"payload": "^1.13.3", "payload": "^1.13.3",
"qs": "^6.11.2", "styled-components": "^6.0.7"
"slugify": "^1.6.6",
"styled-components": "^6.0.7",
"unset-value": "^2.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/dotenv": "^8.2.0", "@types/dotenv": "^8.2.0",

View File

@ -1,10 +1,13 @@
import { CollectionConfig } from "payload/types"; import { CollectionConfig } from "payload/types";
import { mustBeAdmin } from "../../accesses/mustBeAdmin"; import { mustBeAdmin } from "../../accesses/mustBeAdmin";
import { CollectionGroups, Collections } from "../../constants"; import { CollectionGroups, Collections } from "../../constants";
import { backPropagationField } from "../../fields/backPropagationField/backPropagationField";
import { slugField } from "../../fields/slugField/slugField"; import { slugField } from "../../fields/slugField/slugField";
import { localizedFields } from "../../fields/translatedFields/translatedFields"; import { translatedFields } from "../../fields/translatedFields/translatedFields";
import { buildCollectionConfig } from "../../utils/collectionConfig"; import { buildCollectionConfig } from "../../utils/collectionConfig";
import { importFromStrapi } from "./endpoints/importFromStrapi"; import { importFromStrapi } from "./endpoints/importFromStrapi";
import { beforeValidateEndingGreaterThanStarting } from "./hooks/beforeValidateEndingGreaterThanStarting";
import { beforeValidateNoIntersection } from "./hooks/beforeValidateNoIntersection";
const fields = { const fields = {
slug: "slug", slug: "slug",
@ -13,58 +16,71 @@ const fields = {
translations: "translations", translations: "translations",
translationsTitle: "title", translationsTitle: "title",
translationsDescription: "description", translationsDescription: "description",
events: "events",
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const ChronologyEras: CollectionConfig = buildCollectionConfig( export const ChronologyEras: CollectionConfig = buildCollectionConfig({
Collections.ChronologyEras, slug: Collections.ChronologyEras,
{ labels: {
singular: "Chronology Era", singular: "Chronology Era",
plural: "Chronology Eras", plural: "Chronology Eras",
}, },
() => ({ defaultSort: fields.startingYear,
defaultSort: fields.startingYear, admin: {
admin: { group: CollectionGroups.Collections,
group: CollectionGroups.Collections, defaultColumns: [fields.slug, fields.startingYear, fields.endingYear, fields.translations],
defaultColumns: [fields.slug, fields.startingYear, fields.endingYear, fields.translations], useAsTitle: fields.slug,
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: { translatedFields({
create: mustBeAdmin, name: fields.translations,
delete: mustBeAdmin, admin: { useAsTitle: fields.translationsTitle },
}, fields: [
endpoints: [importFromStrapi], { name: fields.translationsTitle, type: "text", required: true },
fields: [ {
slugField({ name: fields.slug }), name: fields.translationsDescription,
{ type: "textarea",
type: "row", },
fields: [ ],
{ }),
name: fields.startingYear, backPropagationField({
type: "number", name: fields.events,
min: 0, hasMany: true,
required: true, relationTo: Collections.ChronologyItems,
admin: { width: "50%", description: "The year the era started (year included)" }, where: ({ startingYear, endingYear }) => ({
}, and: [
{ { "date.year": { greater_than_equal: startingYear } },
name: fields.endingYear, { "date.year": { less_than_equal: 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",
},
], ],
}), }),
], }),
}) ],
); });

View File

@ -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.");
}
};

View File

@ -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<ChronologyEra> = 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})`
);
}
});
};

View File

@ -6,115 +6,137 @@ import {
publishStatusFilters, publishStatusFilters,
} from "../../components/QuickFilters"; } from "../../components/QuickFilters";
import { CollectionGroups, Collections } from "../../constants"; import { CollectionGroups, Collections } from "../../constants";
import { localizedFields } from "../../fields/translatedFields/translatedFields"; import { translatedFields } from "../../fields/translatedFields/translatedFields";
import { isDefined, isUndefined } from "../../utils/asserts"; import { isEmpty, isUndefined } from "../../utils/asserts";
import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig"; import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig";
import { importFromStrapi } from "./endpoints/importFromStrapi"; import { importFromStrapi } from "./endpoints/importFromStrapi";
import { beforeValidatePopulateNameField } from "./hooks/beforeValidatePopulateNameField";
const fields = { const fields = {
name: "name", name: "name",
events: "events", events: "events",
eventsSource: "source",
eventsTranslations: "translations", eventsTranslations: "translations",
eventsTranslationsTitle: "title", eventsTranslationsTitle: "title",
eventsTranslationsDescription: "description", eventsTranslationsDescription: "description",
eventsTranslationsNotes: "notes", eventsTranslationsNotes: "notes",
date: "date", date: "date",
year: "year", dateYear: "year",
month: "month", dateMonth: "month",
day: "day", dateDay: "day",
era: "era",
status: "_status", status: "_status",
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const ChronologyItems: CollectionConfig = buildVersionedCollectionConfig( export const ChronologyItems: CollectionConfig = buildVersionedCollectionConfig({
Collections.ChronologyItems, slug: Collections.ChronologyItems,
{ labels: {
singular: "Chronology Item", singular: "Chronology Item",
plural: "Chronology Items", plural: "Chronology Items",
}, },
() => ({ defaultSort: fields.name,
defaultSort: fields.name, admin: {
admin: { group: CollectionGroups.Collections,
group: CollectionGroups.Collections, defaultColumns: [fields.name, fields.events, fields.status],
defaultColumns: [fields.name, fields.events, fields.status], useAsTitle: fields.name,
useAsTitle: fields.name, components: {
components: { BeforeListTable: [
BeforeListTable: [ () =>
() => QuickFilters({
QuickFilters({ slug: Collections.ChronologyItems,
slug: Collections.ChronologyItems, filterGroups: [
filterGroups: [ languageBasedFilters("events.translations.language"),
languageBasedFilters("events.translations.language"), publishStatusFilters,
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" },
], ],
}), }),
], ],
},
},
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" },
],
}),
],
},
],
});

View File

@ -11,7 +11,7 @@ export const importFromStrapi = createStrapiImportEndpoint<ChronologyItem>({
}, },
payload: { payload: {
collection: Collections.ChronologyItems, collection: Collections.ChronologyItems,
convert: ({ year, month, day, events }) => ({ convert: ({ year, month, day, events }, user) => ({
date: { year, month, day }, date: { year, month, day },
events: events.map((event) => ({ events: events.map((event) => ({
translations: event.translations.map(({ title, description, note, language }) => ({ translations: event.translations.map(({ title, description, note, language }) => ({
@ -19,6 +19,10 @@ export const importFromStrapi = createStrapiImportEndpoint<ChronologyItem>({
description, description,
note, note,
language: language.data.attributes.code, language: language.data.attributes.code,
sourceLanguage: "en",
...(language.data.attributes.code === "en"
? { transcribers: [user.id] }
: { translators: [user.id] }),
})), })),
})), })),
}), }),

View File

@ -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;
};

View File

@ -1,8 +1,9 @@
import { CollectionGroups, Collections, FileTypes, KeysTypes } from "../../constants"; import { CollectionGroups, Collections, FileTypes, KeysTypes } from "../../constants";
import { fileField } from "../../fields/fileField/fileField"; import { fileField } from "../../fields/fileField/fileField";
import { imageField } from "../../fields/imageField/imageField"; import { imageField } from "../../fields/imageField/imageField";
import { keysField } from "../../fields/keysField/keysField";
import { slugField } from "../../fields/slugField/slugField"; import { slugField } from "../../fields/slugField/slugField";
import { localizedFields } from "../../fields/translatedFields/translatedFields"; import { translatedFields } from "../../fields/translatedFields/translatedFields";
import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo"; import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo";
import { beforeDuplicatePiping } from "../../hooks/beforeDuplicatePiping"; import { beforeDuplicatePiping } from "../../hooks/beforeDuplicatePiping";
import { beforeDuplicateUnpublish } from "../../hooks/beforeDuplicateUnpublish"; import { beforeDuplicateUnpublish } from "../../hooks/beforeDuplicateUnpublish";
@ -33,190 +34,184 @@ const fields = {
updatedBy: "updatedBy", updatedBy: "updatedBy",
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const Contents = buildVersionedCollectionConfig( export const Contents = buildVersionedCollectionConfig({
Collections.Contents, slug: Collections.Contents,
{ labels: {
singular: "Content", singular: "Content",
plural: "Contents", plural: "Contents",
}, },
() => ({ defaultSort: fields.slug,
defaultSort: fields.slug, admin: {
admin: { useAsTitle: fields.slug,
useAsTitle: fields.slug, description:
description: "All the contents (textual, audio, and video) from the Library or other online sources.",
"All the contents (textual, audio, and video) from the Library or other online sources.", defaultColumns: [
defaultColumns: [ fields.slug,
fields.slug, fields.thumbnail,
fields.thumbnail, fields.categories,
fields.categories, fields.type,
fields.type, fields.translations,
fields.translations, fields.status,
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%" },
},
],
},
],
},
],
},
],
}),
], ],
}) 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%" },
},
],
},
],
},
],
},
],
}),
],
});

View File

@ -1,6 +1,6 @@
import { CollectionGroups, Collections } from "../../constants"; import { CollectionGroups, Collections } from "../../constants";
import { slugField } from "../../fields/slugField/slugField"; import { slugField } from "../../fields/slugField/slugField";
import { localizedFields } from "../../fields/translatedFields/translatedFields"; import { translatedFields } from "../../fields/translatedFields/translatedFields";
import { buildCollectionConfig } from "../../utils/collectionConfig"; import { buildCollectionConfig } from "../../utils/collectionConfig";
const fields = { const fields = {
@ -11,51 +11,49 @@ const fields = {
contents: "contents", contents: "contents",
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const ContentsFolders = buildCollectionConfig( export const ContentsFolders = buildCollectionConfig({
Collections.ContentsFolders, slug: Collections.ContentsFolders,
{ labels: {
singular: "Contents Folder", singular: "Contents Folder",
plural: "Contents Folders", plural: "Contents Folders",
}, },
() => ({ defaultSort: fields.slug,
defaultSort: fields.slug, admin: {
admin: { useAsTitle: fields.slug,
useAsTitle: fields.slug, defaultColumns: [fields.slug, fields.translations],
defaultColumns: [fields.slug, fields.translations], disableDuplicate: true,
disableDuplicate: true, group: CollectionGroups.Collections,
group: CollectionGroups.Collections, },
}, timestamps: false,
timestamps: false, versions: false,
versions: false, fields: [
fields: [ slugField({ name: fields.slug }),
slugField({ name: fields.slug }), translatedFields({
localizedFields({ name: fields.translations,
name: fields.translations, interfaceName: "ContentFoldersTranslation",
interfaceName: "ContentFoldersTranslation", admin: {
admin: { useAsTitle: fields.name,
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%" },
},
],
}, },
], 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%" },
},
],
},
],
});

View File

@ -1,5 +1,5 @@
import { CollectionGroups, Collections } from "../../constants"; import { CollectionGroups, Collections } from "../../constants";
import { buildCollectionConfig } from "../../utils/collectionConfig"; import { buildImageCollectionConfig } from "../../utils/imageCollectionConfig";
const fields = { const fields = {
filename: "filename", filename: "filename",
@ -7,43 +7,39 @@ const fields = {
filesize: "filesize", filesize: "filesize",
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const ContentsThumbnails = buildCollectionConfig( export const ContentsThumbnails = buildImageCollectionConfig({
Collections.ContentsThumbnails, slug: Collections.ContentsThumbnails,
{ labels: {
singular: "Contents Thumbnail", singular: "Contents Thumbnail",
plural: "Contents Thumbnails", plural: "Contents Thumbnails",
}, },
({ uploadDir }) => ({ defaultSort: fields.filename,
defaultSort: fields.filename, admin: {
admin: { useAsTitle: fields.filename,
useAsTitle: fields.filename, disableDuplicate: true,
disableDuplicate: true, group: CollectionGroups.Media,
group: CollectionGroups.Media, },
}, upload: {
upload: { imageSizes: [
staticDir: uploadDir, {
mimeTypes: ["image/*"], name: "og",
imageSizes: [ height: 750,
{ width: 1125,
name: "og", formatOptions: {
height: 750, format: "jpg",
width: 1125, options: { progressive: true, mozjpeg: true, compressionLevel: 9, quality: 80 },
formatOptions: {
format: "jpg",
options: { progressive: true, mozjpeg: true, compressionLevel: 9, quality: 80 },
},
}, },
{ },
name: "medium", {
height: 1000, name: "medium",
width: 1500, height: 1000,
formatOptions: { width: 1500,
format: "webp", formatOptions: {
options: { effort: 6, quality: 80, alphaQuality: 80 }, format: "webp",
}, options: { effort: 6, quality: 80, alphaQuality: 80 },
}, },
], },
}, ],
fields: [], },
}) fields: [],
); });

View File

@ -8,37 +8,35 @@ const fields = {
id: "id", id: "id",
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const Currencies = buildCollectionConfig( export const Currencies = buildCollectionConfig({
Collections.Currencies, slug: Collections.Currencies,
{ labels: {
singular: "Currency", singular: "Currency",
plural: "Currencies", plural: "Currencies",
}, },
() => ({ defaultSort: fields.id,
defaultSort: fields.id, admin: {
admin: { pagination: { defaultLimit: 100 },
pagination: { defaultLimit: 100 }, useAsTitle: fields.id,
useAsTitle: fields.id, defaultColumns: [fields.id],
defaultColumns: [fields.id], disableDuplicate: true,
disableDuplicate: true, group: CollectionGroups.Meta,
group: CollectionGroups.Meta, },
}, access: { create: mustBeAdmin, update: mustBeAdmin },
access: { create: mustBeAdmin, update: mustBeAdmin }, endpoints: [importFromStrapi],
endpoints: [importFromStrapi], timestamps: false,
timestamps: false, fields: [
fields: [ {
{ name: fields.id,
name: fields.id, type: "text",
type: "text", unique: true,
unique: true, required: true,
required: true, validate: (value, options) => {
validate: (value, options) => { if (!/^[A-Z]{3}$/g.test(value)) {
if (!/^[A-Z]{3}$/g.test(value)) { return "The code must be a valid ISO 4217 currency code (e.g: EUR, CAD...)";
return "The code must be a valid ISO 4217 currency code (e.g: EUR, CAD...)"; }
} return text(value, options);
return text(value, options);
},
}, },
], },
}) ],
); });

View File

@ -6,31 +6,29 @@ const fields = {
type: "type", type: "type",
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const Files = buildCollectionConfig( export const Files = buildCollectionConfig({
Collections.Files, slug: Collections.Files,
{ labels: {
singular: "File", singular: "File",
plural: "Files", plural: "Files",
}, },
() => ({ defaultSort: fields.filename,
defaultSort: fields.filename, admin: {
admin: { useAsTitle: fields.filename,
useAsTitle: fields.filename, disableDuplicate: true,
disableDuplicate: true, group: CollectionGroups.Media,
group: CollectionGroups.Media, },
fields: [
{
name: fields.filename,
required: true,
type: "text",
}, },
fields: [ {
{ name: fields.type,
name: fields.filename, type: "select",
required: true, required: true,
type: "text", 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 })),
},
],
})
);

View File

@ -2,7 +2,7 @@ import payload from "payload";
import { mustBeAdmin } from "../../accesses/mustBeAdmin"; import { mustBeAdmin } from "../../accesses/mustBeAdmin";
import { QuickFilters } from "../../components/QuickFilters"; import { QuickFilters } from "../../components/QuickFilters";
import { CollectionGroups, Collections, KeysTypes, LanguageCodes } from "../../constants"; 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 { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo";
import { Key } from "../../types/collections"; import { Key } from "../../types/collections";
import { isDefined } from "../../utils/asserts"; import { isDefined } from "../../utils/asserts";
@ -19,103 +19,101 @@ const fields = {
const keysTypesWithShort: (keyof typeof KeysTypes)[] = ["Categories", "GamePlatforms"]; const keysTypesWithShort: (keyof typeof KeysTypes)[] = ["Categories", "GamePlatforms"];
export const Keys = buildCollectionConfig( export const Keys = buildCollectionConfig({
Collections.Keys, slug: Collections.Keys,
{ labels: {
singular: "Key", singular: "Key",
plural: "Keys", plural: "Keys",
}, },
() => ({ defaultSort: fields.name,
defaultSort: fields.name, admin: {
admin: { useAsTitle: fields.name,
useAsTitle: fields.name, defaultColumns: [fields.name, fields.type, fields.translations],
defaultColumns: [fields.name, fields.type, fields.translations], group: CollectionGroups.Meta,
group: CollectionGroups.Meta, components: {
components: { BeforeListTable: [
BeforeListTable: [ () =>
() => QuickFilters({
QuickFilters({ slug: Collections.Keys,
slug: Collections.Keys, filterGroups: [
filterGroups: [ Object.entries(KeysTypes).map(([key, value]) => ({
Object.entries(KeysTypes).map(([key, value]) => ({ label: value,
label: value, filter: { where: { type: { equals: key } } },
filter: { where: { type: { equals: key } } }, })),
})), Object.entries(LanguageCodes).map(([key, value]) => ({
Object.entries(LanguageCodes).map(([key, value]) => ({ label: `${value}`,
label: `${value}`, filter: { where: { "translations.language": { not_equals: key } } },
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}"`
);
}
},
], ],
}, },
endpoints: [importFromStrapi], hooks: {
timestamps: false, beforeDuplicate: beforeDuplicateAddCopyTo(fields.name),
versions: false, },
fields: [ },
{ access: {
name: fields.name, create: mustBeAdmin,
type: "text", delete: mustBeAdmin,
required: true, },
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<Key>) =>
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<Key>) =>
isDefined(data.type) && keysTypesWithShort.includes(data.type),
width: "50%",
},
},
],
},
],
}),
],
});

View File

@ -10,43 +10,42 @@ const fields = {
name: "name", name: "name",
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const Languages = buildCollectionConfig( export const Languages = buildCollectionConfig({
Collections.Languages, slug: Collections.Languages,
{ labels: {
singular: "Language", singular: "Language",
plural: "Languages", plural: "Languages",
}, },
() => ({ defaultSort: fields.name,
defaultSort: fields.name, admin: {
admin: { useAsTitle: fields.name,
useAsTitle: fields.name, defaultColumns: [fields.name, fields.id],
defaultColumns: [fields.name, fields.id], disableDuplicate: true,
disableDuplicate: true, group: CollectionGroups.Meta,
group: CollectionGroups.Meta, pagination: { defaultLimit: 100 },
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, name: fields.name,
endpoints: [importFromStrapi], type: "text",
fields: [ unique: true,
{ required: true,
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,
},
],
})
);

File diff suppressed because it is too large Load Diff

View File

@ -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<string, string>;
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: [],
});

View File

@ -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<string, string>;
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: [],
});

View File

@ -1,5 +1,5 @@
import { CollectionGroups, Collections } from "../../constants"; import { CollectionGroups, Collections } from "../../constants";
import { buildCollectionConfig } from "../../utils/collectionConfig"; import { buildImageCollectionConfig } from "../../utils/imageCollectionConfig";
const fields = { const fields = {
filename: "filename", filename: "filename",
@ -7,55 +7,49 @@ const fields = {
filesize: "filesize", filesize: "filesize",
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const LibraryItemsThumbnails = buildCollectionConfig( export const LibraryItemsThumbnails = buildImageCollectionConfig({
Collections.LibraryItemsThumbnails, slug: Collections.LibraryItemsThumbnails,
{ labels: {
singular: "Library Item Thumbnail", singular: "Library Item Thumbnail",
plural: "Library Item Thumbnails", plural: "Library Item Thumbnails",
}, },
({ uploadDir }) => ({ defaultSort: fields.filename,
defaultSort: fields.filename, admin: {
admin: { useAsTitle: fields.filename,
useAsTitle: fields.filename, disableDuplicate: true,
disableDuplicate: true, group: CollectionGroups.Media,
group: CollectionGroups.Media, },
}, upload: {
upload: { imageSizes: [
staticDir: uploadDir, {
mimeTypes: ["image/*"], name: "og",
imageSizes: [ height: 1024,
{ width: 1024,
name: "og", fit: "inside",
height: 1024, formatOptions: {
width: 1024, format: "jpg",
fit: "contain", options: { progressive: true, mozjpeg: true, compressionLevel: 9, quality: 80 },
formatOptions: {
format: "jpg",
options: { progressive: true, mozjpeg: true, compressionLevel: 9, quality: 80 },
},
}, },
{ },
name: "medium", {
height: 1024, name: "square",
width: 1024, height: 1024,
fit: "contain", width: 1024,
formatOptions: { fit: "contain",
format: "webp", background: { r: 0, g: 0, b: 0, alpha: 0 },
options: { effort: 6, quality: 80, alphaQuality: 80 }, formatOptions: {
}, format: "webp",
options: { effort: 6, quality: 80, alphaQuality: 80 },
}, },
{ },
name: "large", {
height: 2048, name: "max",
width: 2048, formatOptions: {
fit: "contain", format: "webp",
formatOptions: { options: { effort: 6, quality: 80, alphaQuality: 80 },
format: "webp",
options: { effort: 6, quality: 80, alphaQuality: 80 },
},
}, },
], },
}, ],
fields: [], },
}) fields: [],
); });

View File

@ -2,7 +2,7 @@ import { QuickFilters, publishStatusFilters } from "../../components/QuickFilter
import { CollectionGroups, Collections, KeysTypes } from "../../constants"; import { CollectionGroups, Collections, KeysTypes } from "../../constants";
import { imageField } from "../../fields/imageField/imageField"; import { imageField } from "../../fields/imageField/imageField";
import { slugField } from "../../fields/slugField/slugField"; import { slugField } from "../../fields/slugField/slugField";
import { localizedFields } from "../../fields/translatedFields/translatedFields"; import { translatedFields } from "../../fields/translatedFields/translatedFields";
import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo"; import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo";
import { beforeDuplicatePiping } from "../../hooks/beforeDuplicatePiping"; import { beforeDuplicatePiping } from "../../hooks/beforeDuplicatePiping";
import { beforeDuplicateUnpublish } from "../../hooks/beforeDuplicateUnpublish"; import { beforeDuplicateUnpublish } from "../../hooks/beforeDuplicateUnpublish";
@ -26,151 +26,146 @@ const fields = {
proofreaders: "proofreaders", proofreaders: "proofreaders",
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const Posts = buildVersionedCollectionConfig( export const Posts = buildVersionedCollectionConfig({
Collections.Posts, slug: Collections.Posts,
{ labels: {
singular: "Post", singular: "Post",
plural: "Posts", plural: "Posts",
}, },
() => ({ defaultSort: fields.slug,
defaultSort: fields.slug, admin: {
admin: { useAsTitle: fields.slug,
useAsTitle: fields.slug, description:
description: "News articles written by our Recorders! Here you will find announcements about \
"News articles written by our Recorders! Here you will find announcements about \
new merch/items releases, guides, theories, unboxings, showcases...", new merch/items releases, guides, theories, unboxings, showcases...",
defaultColumns: [fields.slug, fields.thumbnail, fields.categories], defaultColumns: [fields.slug, fields.thumbnail, fields.categories],
group: CollectionGroups.Collections, group: CollectionGroups.Collections,
components: { components: {
BeforeListTable: [ BeforeListTable: [
() => () =>
QuickFilters({ QuickFilters({
slug: Collections.Posts, slug: Collections.Posts,
filterGroups: [publishStatusFilters], filterGroups: [publishStatusFilters],
}), }),
], ],
},
hooks: {
beforeDuplicate: beforeDuplicatePiping([
beforeDuplicateUnpublish,
beforeDuplicateAddCopyTo(fields.slug),
]),
},
preview: (doc) => `https://accords-library.com/news/${doc.slug}`,
}, },
hooks: { hooks: {
beforeValidate: [removeTranslatorsForTranscripts], beforeDuplicate: beforeDuplicatePiping([
beforeDuplicateUnpublish,
beforeDuplicateAddCopyTo(fields.slug),
]),
}, },
fields: [ preview: (doc) => `https://accords-library.com/news/${doc.slug}`,
{ },
type: "row", hooks: {
fields: [ beforeValidate: [removeTranslatorsForTranscripts],
slugField({ name: fields.slug, admin: { width: "50%" } }), },
imageField({ fields: [
name: fields.thumbnail, {
relationTo: Collections.PostsThumbnails, type: "row",
admin: { width: "50%" }, fields: [
}), slugField({ name: fields.slug, admin: { width: "50%" } }),
], imageField({
}, name: fields.thumbnail,
{ relationTo: Collections.PostsThumbnails,
type: "row", admin: { width: "50%" },
fields: [ }),
{ ],
name: fields.authors, },
type: "relationship", {
relationTo: [Collections.Recorders], type: "row",
required: true, fields: [
minRows: 1, {
hasMany: true, name: fields.authors,
admin: { width: "35%" }, type: "relationship",
}, relationTo: [Collections.Recorders],
{ required: true,
name: fields.categories, minRows: 1,
type: "relationship", hasMany: true,
relationTo: [Collections.Keys], admin: { width: "35%" },
filterOptions: { type: { equals: KeysTypes.Categories } }, },
hasMany: true, {
admin: { allowCreate: false, width: "35%" }, name: fields.categories,
}, type: "relationship",
], relationTo: [Collections.Keys],
}, filterOptions: { type: { equals: KeysTypes.Categories } },
localizedFields({ hasMany: true,
name: fields.translations, admin: { allowCreate: false, width: "35%" },
admin: { useAsTitle: fields.title, hasSourceLanguage: true }, },
required: true, ],
minRows: 1, },
fields: [ translatedFields({
{ name: fields.title, type: "text", required: true }, name: fields.translations,
{ name: fields.summary, type: "textarea" }, admin: { useAsTitle: fields.title, hasSourceLanguage: true },
{ required: true,
type: "row", minRows: 1,
fields: [ fields: [
{ { name: fields.title, type: "text", required: true },
name: fields.translators, { name: fields.summary, type: "textarea" },
type: "relationship", {
relationTo: Collections.Recorders, type: "row",
hasMany: true, fields: [
admin: { {
condition: (_, siblingData) => { name: fields.translators,
if ( type: "relationship",
isUndefined(siblingData.language) || relationTo: Collections.Recorders,
isUndefined(siblingData.sourceLanguage) hasMany: true,
) { admin: {
return false; condition: (_, siblingData) => {
}
return siblingData.language !== siblingData.sourceLanguage;
},
width: "50%",
},
validate: (translators, { siblingData }) => {
if ( if (
isUndefined(siblingData.language) || isUndefined(siblingData.language) ||
isUndefined(siblingData.sourceLanguage) isUndefined(siblingData.sourceLanguage)
) { ) {
return true; return false;
} }
if (siblingData.language === siblingData.sourceLanguage) { return siblingData.language !== siblingData.sourceLanguage;
return true;
}
if (isDefined(translators) && translators.length > 0) {
return true;
}
return "This field is required when the language is different from the source language.";
}, },
width: "50%",
}, },
{ validate: (translators, { siblingData }) => {
name: fields.proofreaders, if (isUndefined(siblingData.language) || isUndefined(siblingData.sourceLanguage)) {
type: "relationship", return true;
relationTo: Collections.Recorders, }
hasMany: true, if (siblingData.language === siblingData.sourceLanguage) {
admin: { width: "50%" }, 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.proofreaders,
], type: "relationship",
}), relationTo: Collections.Recorders,
{ hasMany: true,
name: fields.publishedDate, admin: { width: "50%" },
type: "date", },
defaultValue: new Date().toISOString(), ],
admin: {
date: { pickerAppearance: "dayOnly", displayFormat: "yyyy-MM-dd" },
position: "sidebar",
}, },
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",
}, },
{ required: true,
name: fields.hidden, },
type: "checkbox", {
required: false, name: fields.hidden,
defaultValue: false, type: "checkbox",
admin: { required: false,
description: "If enabled, the post won't appear in the 'News' section", defaultValue: false,
position: "sidebar", admin: {
}, description: "If enabled, the post won't appear in the 'News' section",
position: "sidebar",
}, },
], },
}) ],
); });

View File

@ -1,5 +1,5 @@
import { CollectionGroups, Collections } from "../../constants"; import { CollectionGroups, Collections } from "../../constants";
import { buildCollectionConfig } from "../../utils/collectionConfig"; import { buildImageCollectionConfig } from "../../utils/imageCollectionConfig";
const fields = { const fields = {
filename: "filename", filename: "filename",
@ -7,43 +7,39 @@ const fields = {
filesize: "filesize", filesize: "filesize",
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const PostsThumbnails = buildCollectionConfig( export const PostsThumbnails = buildImageCollectionConfig({
Collections.PostsThumbnails, slug: Collections.PostsThumbnails,
{ labels: {
singular: "Post Thumbnail", singular: "Post Thumbnail",
plural: "Post Thumbnails", plural: "Post Thumbnails",
}, },
({ uploadDir }) => ({ defaultSort: fields.filename,
defaultSort: fields.filename, admin: {
admin: { useAsTitle: fields.filename,
useAsTitle: fields.filename, disableDuplicate: true,
disableDuplicate: true, group: CollectionGroups.Media,
group: CollectionGroups.Media, },
}, upload: {
upload: { imageSizes: [
staticDir: uploadDir, {
mimeTypes: ["image/*"], name: "og",
imageSizes: [ height: 750,
{ width: 1125,
name: "og", formatOptions: {
height: 750, format: "jpg",
width: 1125, options: { progressive: true, mozjpeg: true, compressionLevel: 9, quality: 80 },
formatOptions: {
format: "jpg",
options: { progressive: true, mozjpeg: true, compressionLevel: 9, quality: 80 },
},
}, },
{ },
name: "medium", {
height: 1000, name: "medium",
width: 1500, height: 1000,
formatOptions: { width: 1500,
format: "webp", formatOptions: {
options: { effort: 6, quality: 80, alphaQuality: 80 }, format: "webp",
}, options: { effort: 6, quality: 80, alphaQuality: 80 },
}, },
], },
}, ],
fields: [], },
}) fields: [],
); });

View File

@ -3,7 +3,7 @@ import { mustBeAdmin } from "../../accesses/mustBeAdmin";
import { QuickFilters } from "../../components/QuickFilters"; import { QuickFilters } from "../../components/QuickFilters";
import { CollectionGroups, Collections, RecordersRoles } from "../../constants"; import { CollectionGroups, Collections, RecordersRoles } from "../../constants";
import { imageField } from "../../fields/imageField/imageField"; import { imageField } from "../../fields/imageField/imageField";
import { localizedFields } from "../../fields/translatedFields/translatedFields"; import { translatedFields } from "../../fields/translatedFields/translatedFields";
import { buildCollectionConfig } from "../../utils/collectionConfig"; import { buildCollectionConfig } from "../../utils/collectionConfig";
import { importFromStrapi } from "./endpoints/importFromStrapi"; import { importFromStrapi } from "./endpoints/importFromStrapi";
import { beforeLoginMustHaveAtLeastOneRole } from "./hooks/beforeLoginMustHaveAtLeastOneRole"; import { beforeLoginMustHaveAtLeastOneRole } from "./hooks/beforeLoginMustHaveAtLeastOneRole";
@ -18,127 +18,125 @@ const fields = {
role: "role", role: "role",
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const Recorders = buildCollectionConfig( export const Recorders = buildCollectionConfig({
Collections.Recorders, slug: Collections.Recorders,
{ labels: {
singular: "Recorder", singular: "Recorder",
plural: "Recorders", plural: "Recorders",
}, },
() => ({ defaultSort: fields.username,
defaultSort: fields.username, admin: {
admin: { useAsTitle: fields.username,
useAsTitle: fields.username, description:
description: "Recorders are contributors of the Accord's Library project. Ask an admin to create a \
"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.", Recorder here to be able to credit them in other collections.",
defaultColumns: [ defaultColumns: [
fields.username, fields.username,
fields.avatar, fields.avatar,
fields.anonymize, fields.anonymize,
fields.biographies, fields.biographies,
fields.languages, fields.languages,
fields.role, 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",
},
},
], ],
}) 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",
},
},
],
});

View File

@ -1,6 +1,6 @@
import { CollectionGroups, Collections } from "../../constants"; import { CollectionGroups, Collections } from "../../constants";
import { backPropagationField } from "../../fields/backPropagationField/backPropagationField"; import { backPropagationField } from "../../fields/backPropagationField/backPropagationField";
import { buildCollectionConfig } from "../../utils/collectionConfig"; import { buildImageCollectionConfig } from "../../utils/imageCollectionConfig";
const fields = { const fields = {
filename: "filename", filename: "filename",
@ -9,51 +9,46 @@ const fields = {
recorder: "recorder", recorder: "recorder",
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const RecordersThumbnails = buildCollectionConfig( export const RecordersThumbnails = buildImageCollectionConfig({
Collections.RecordersThumbnails, slug: Collections.RecordersThumbnails,
{ labels: {
singular: "Recorders Thumbnail", singular: "Recorders Thumbnail",
plural: "Recorders Thumbnails", plural: "Recorders Thumbnails",
}, },
({ uploadDir }) => ({ defaultSort: fields.filename,
defaultSort: fields.filename, admin: {
admin: { useAsTitle: fields.filename,
useAsTitle: fields.filename, disableDuplicate: true,
disableDuplicate: true, group: CollectionGroups.Media,
group: CollectionGroups.Media, },
}, upload: {
upload: { imageSizes: [
staticDir: uploadDir, {
adminThumbnail: "small", name: "og",
mimeTypes: ["image/*"], height: 256,
imageSizes: [ width: 256,
{ formatOptions: {
name: "og", format: "jpg",
height: 256, options: { progressive: true, mozjpeg: true, compressionLevel: 9, quality: 80 },
width: 256,
formatOptions: {
format: "jpg",
options: { progressive: true, mozjpeg: true, compressionLevel: 9, quality: 80 },
},
}, },
{ },
name: "small", {
height: 128, name: "small",
width: 128, height: 128,
formatOptions: { width: 128,
format: "webp", formatOptions: {
options: { effort: 6, quality: 80, alphaQuality: 80 }, 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 } }),
}),
],
});

View File

@ -17,87 +17,85 @@ const fields = {
channel: "channel", channel: "channel",
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const Videos: CollectionConfig = buildCollectionConfig( export const Videos: CollectionConfig = buildCollectionConfig({
Collections.Videos, slug: Collections.Videos,
{ labels: {
singular: "Video", singular: "Video",
plural: "Videos", plural: "Videos",
}, },
() => ({ defaultSort: fields.uid,
defaultSort: fields.uid, admin: {
admin: { useAsTitle: fields.title,
useAsTitle: fields.title, defaultColumns: [
defaultColumns: [ fields.uid,
fields.uid, fields.title,
fields.title, fields.source,
fields.source, fields.gone,
fields.gone, fields.liveChat,
fields.liveChat, fields.publishedDate,
fields.publishedDate, fields.views,
fields.views, fields.likes,
fields.likes, fields.channel,
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" },
},
], ],
}) 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" },
},
],
});

View File

@ -11,35 +11,33 @@ const fields = {
videos: "videos", videos: "videos",
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const VideosChannels: CollectionConfig = buildCollectionConfig( export const VideosChannels: CollectionConfig = buildCollectionConfig({
Collections.VideosChannels, slug: Collections.VideosChannels,
{ labels: {
singular: "Videos Channel", singular: "Videos Channel",
plural: "Videos Channels", plural: "Videos Channels",
}, },
() => ({ defaultSort: fields.title,
defaultSort: fields.title, admin: {
admin: { useAsTitle: fields.title,
useAsTitle: fields.title, defaultColumns: [fields.uid, fields.title, fields.subscribers, fields.videos],
defaultColumns: [fields.uid, fields.title, fields.subscribers, fields.videos], group: CollectionGroups.Media,
group: CollectionGroups.Media, disableDuplicate: true,
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" },
],
},
],
})
);

View File

@ -1,7 +1,8 @@
import { CollectionGroups, Collections, KeysTypes } from "../../constants"; import { CollectionGroups, Collections, KeysTypes } from "../../constants";
import { imageField } from "../../fields/imageField/imageField"; import { imageField } from "../../fields/imageField/imageField";
import { keysField } from "../../fields/keysField/keysField";
import { slugField } from "../../fields/slugField/slugField"; import { slugField } from "../../fields/slugField/slugField";
import { localizedFields } from "../../fields/translatedFields/translatedFields"; import { translatedFields } from "../../fields/translatedFields/translatedFields";
import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig"; import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig";
import { AppearanceRowLabel } from "./components/AppearanceRowLabel"; import { AppearanceRowLabel } from "./components/AppearanceRowLabel";
import { importFromStrapi } from "./endpoints/importFromStrapi"; import { importFromStrapi } from "./endpoints/importFromStrapi";
@ -23,141 +24,135 @@ const fields = {
status: "_status", status: "_status",
}; };
export const Weapons = buildVersionedCollectionConfig( export const Weapons = buildVersionedCollectionConfig({
Collections.Weapons, slug: Collections.Weapons,
{ singular: "Weapon", plural: "Weapons" }, labels: { singular: "Weapon", plural: "Weapons" },
() => ({ defaultSort: fields.slug,
defaultSort: fields.slug, admin: {
admin: { useAsTitle: fields.slug,
useAsTitle: fields.slug, defaultColumns: [
defaultColumns: [ fields.slug,
fields.slug, fields.thumbnail,
fields.thumbnail, fields.group,
fields.group, fields.type,
fields.type, fields.appearances,
fields.appearances, fields.status,
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%" },
},
],
},
],
}),
],
},
], ],
}) 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%" },
},
],
},
],
}),
],
},
],
});

View File

@ -1,7 +1,7 @@
import { CollectionGroups, Collections } from "../../constants"; import { CollectionGroups, Collections } from "../../constants";
import { backPropagationField } from "../../fields/backPropagationField/backPropagationField"; import { backPropagationField } from "../../fields/backPropagationField/backPropagationField";
import { slugField } from "../../fields/slugField/slugField"; import { slugField } from "../../fields/slugField/slugField";
import { localizedFields } from "../../fields/translatedFields/translatedFields"; import { translatedFields } from "../../fields/translatedFields/translatedFields";
import { buildCollectionConfig } from "../../utils/collectionConfig"; import { buildCollectionConfig } from "../../utils/collectionConfig";
const fields = { const fields = {
@ -12,30 +12,28 @@ const fields = {
weapons: "weapons", weapons: "weapons",
}; };
export const WeaponsGroups = buildCollectionConfig( export const WeaponsGroups = buildCollectionConfig({
Collections.WeaponsGroups, slug: Collections.WeaponsGroups,
{ singular: "Weapons Group", plural: "Weapon Groups" }, labels: { singular: "Weapons Group", plural: "Weapon Groups" },
() => ({ defaultSort: fields.slug,
defaultSort: fields.slug, admin: {
admin: { useAsTitle: fields.slug,
useAsTitle: fields.slug, defaultColumns: [fields.slug, fields.translations, fields.weapons, fields.subgroupOf],
defaultColumns: [fields.slug, fields.translations, fields.weapons, fields.subgroupOf], group: CollectionGroups.Collections,
group: CollectionGroups.Collections, },
}, timestamps: false,
timestamps: false, fields: [
fields: [ slugField({ name: fields.slug }),
slugField({ name: fields.slug }), translatedFields({
localizedFields({ name: fields.translations,
name: fields.translations, admin: { useAsTitle: fields.translationsName },
admin: { useAsTitle: fields.translationsName }, fields: [{ name: fields.translationsName, type: "text", required: true }],
fields: [{ name: fields.translationsName, type: "text", required: true }], }),
}), backPropagationField({
backPropagationField({ name: fields.weapons,
name: fields.weapons, relationTo: Collections.Weapons,
relationTo: Collections.Weapons, hasMany: true,
hasMany: true, where: ({ id }) => ({ group: { equals: id } }),
where: (id) => ({ group: { equals: id } }), }),
}), ],
], });
})
);

View File

@ -1,5 +1,5 @@
import { CollectionGroups, Collections } from "../../constants"; import { CollectionGroups, Collections } from "../../constants";
import { buildCollectionConfig } from "../../utils/collectionConfig"; import { buildImageCollectionConfig } from "../../utils/imageCollectionConfig";
const fields = { const fields = {
filename: "filename", filename: "filename",
@ -7,44 +7,60 @@ const fields = {
filesize: "filesize", filesize: "filesize",
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
export const WeaponsThumbnails = buildCollectionConfig( export const WeaponsThumbnails = buildImageCollectionConfig({
Collections.WeaponsThumbnails, slug: Collections.WeaponsThumbnails,
{ labels: {
singular: "Weapons Thumbnail", singular: "Weapons Thumbnail",
plural: "Weapons Thumbnails", plural: "Weapons Thumbnails",
}, },
({ uploadDir }) => ({ defaultSort: fields.filename,
defaultSort: fields.filename, admin: {
admin: { useAsTitle: fields.filename,
useAsTitle: fields.filename, disableDuplicate: true,
disableDuplicate: true, group: CollectionGroups.Media,
group: CollectionGroups.Media, },
}, upload: {
upload: { imageSizes: [
staticDir: uploadDir, {
adminThumbnail: "small", name: "og",
mimeTypes: ["image/*"], height: 512,
imageSizes: [ width: 512,
{ fit: "inside",
name: "og", formatOptions: {
height: 256, format: "jpg",
width: 256, options: { progressive: true, mozjpeg: true, compressionLevel: 9, quality: 80 },
formatOptions: {
format: "jpg",
options: { progressive: true, mozjpeg: true, compressionLevel: 9, quality: 80 },
},
}, },
{ },
name: "small", {
height: 128, name: "small",
width: 128, height: 256,
formatOptions: { width: 256,
format: "webp", fit: "contain",
options: { effort: 6, quality: 80, alphaQuality: 80 }, 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: [],
});

View File

@ -1,4 +1,3 @@
import React from "react";
import { styled } from "styled-components"; import { styled } from "styled-components";
export const Icon = styled.div` export const Icon = styled.div`
@ -9,4 +8,8 @@ export const Icon = styled.div`
mask-size: contain; mask-size: contain;
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: center; mask-position: center;
-webkit-mask: url("/public/accords.svg");
-webkit-mask-size: contain;
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
`; `;

View File

@ -1,5 +1,5 @@
import React from "react";
import "@fontsource/vollkorn/700.css"; import "@fontsource/vollkorn/700.css";
import React from "react";
import { styled } from "styled-components"; import { styled } from "styled-components";
export const Logo = (): JSX.Element => ( export const Logo = (): JSX.Element => (
@ -34,4 +34,8 @@ const Icon = styled.div`
mask-size: contain; mask-size: contain;
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: center; mask-position: center;
-webkit-mask: url("/public/accords.svg");
-webkit-mask-size: contain;
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
`; `;

View File

@ -10,6 +10,8 @@ export enum Collections {
Languages = "languages", Languages = "languages",
LibraryItems = "library-items", LibraryItems = "library-items",
LibraryItemsThumbnails = "library-items-thumbnails", LibraryItemsThumbnails = "library-items-thumbnails",
LibraryItemsScans = "library-items-scans",
LibraryItemsGallery = "library-items-gallery",
Posts = "posts", Posts = "posts",
PostsThumbnails = "posts-thumbnails", PostsThumbnails = "posts-thumbnails",
Recorders = "recorders", Recorders = "recorders",
@ -87,4 +89,5 @@ export enum CollectionStatus {
export enum VideoSources { export enum VideoSources {
YouTube = "YouTube", YouTube = "YouTube",
NicoNico = "NicoNico", NicoNico = "NicoNico",
Tumblr = "Tumblr",
} }

View File

@ -39,7 +39,7 @@ type Params<T> = {
payload: { payload: {
collection: string; collection: string;
import?: (strapiObject: any, user: any) => Promise<void>; import?: (strapiObject: any, user: any) => Promise<void>;
convert?: (strapiObject: any) => PayloadCreateData<T>; convert?: (strapiObject: any, user: any) => PayloadCreateData<T>;
}; };
}; };
@ -60,7 +60,7 @@ export const importStrapiEntries = async <T>({
} else if (isDefined(payloadParams.convert)) { } else if (isDefined(payloadParams.convert)) {
await payload.create({ await payload.create({
collection: payloadParams.collection, collection: payloadParams.collection,
data: payloadParams.convert(attributes), data: payloadParams.convert(attributes, user),
user, user,
}); });
} else { } else {

View File

@ -4,18 +4,20 @@ import { RelationshipField, Where } from "payload/types";
import { isNotEmpty } from "../../utils/asserts"; import { isNotEmpty } from "../../utils/asserts";
type BackPropagationField = FieldBase & { type BackPropagationField = FieldBase & {
where: (id: string) => Where; where: (data: any) => Where;
relationTo: string; relationTo: string;
hasMany: boolean; hasMany?: boolean;
}; };
export const backPropagationField = ({ export const backPropagationField = ({
admin, admin,
hooks: { beforeChange = [], afterRead = [], ...otherHooks } = {}, hooks: { beforeChange = [], afterRead = [], ...otherHooks } = {},
where, where,
hasMany = false,
...params ...params
}: BackPropagationField): RelationshipField => ({ }: BackPropagationField): RelationshipField => ({
...params, ...params,
type: "relationship", type: "relationship",
hasMany: hasMany,
admin: { ...admin, readOnly: true }, admin: { ...admin, readOnly: true },
hooks: { hooks: {
...otherHooks, ...otherHooks,
@ -31,17 +33,17 @@ export const backPropagationField = ({
if (isNotEmpty(data.id)) { if (isNotEmpty(data.id)) {
const result = await payload.find({ const result = await payload.find({
collection: params.relationTo, collection: params.relationTo,
where: where(data.id), where: where(data),
limit: 100, limit: 100,
depth: 0, depth: 0,
}); });
if (params.hasMany) { if (hasMany) {
return result.docs.map((doc) => doc.id); return result.docs.map((doc) => doc.id);
} else { } else {
return result.docs[0].id; return result.docs[0].id;
} }
} }
return params.hasMany ? [] : undefined; return hasMany ? [] : undefined;
}, },
], ],
}, },

View File

@ -1,9 +1,23 @@
import { Props } from "payload/components/views/Cell"; import { Props } from "payload/components/views/Cell";
import { useState, useEffect } from "react"; import React, { useEffect, useMemo, useState } from "react";
import React from "react"; import { Link } from "react-router-dom";
import { styled } from "styled-components";
import { isUndefined } from "../../utils/asserts"; 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<string>(); const [imageURL, setImageURL] = useState<string>();
useEffect(() => { useEffect(() => {
const fetchUrl = async () => { const fetchUrl = async () => {
@ -11,18 +25,14 @@ export const Cell = ({ cellData, field }: Props): JSX.Element => {
if (typeof cellData !== "string") return; if (typeof cellData !== "string") return;
if (field.type !== "upload") return; if (field.type !== "upload") return;
const result = await (await fetch(`/api/${field.relationTo}/${cellData}`)).json(); const result = await (await fetch(`/api/${field.relationTo}/${cellData}`)).json();
setImageURL(result.url); setImageURL(result.sizes.thumb.url);
}; };
fetchUrl(); fetchUrl();
}, [cellData]); }, [cellData]);
const link = useMemo(
return ( () => `/admin/collections/${collection.slug}/${rowData.id}`,
<> [collection.slug, rowData.id]
{imageURL ? (
<img style={{ height: "3rem", borderRadius: "100%", aspectRatio: "1/1" }} src={imageURL} />
) : (
"<No Image>"
)}
</>
); );
return <Link to={link}>{imageURL ? <Image src={imageURL} /> : "<No Image>"}</Link>;
}; };

View File

@ -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];

View File

@ -1,8 +1,7 @@
import { array } from "payload/dist/fields/validations"; import { array } from "payload/dist/fields/validations";
import { ArrayField, Field } from "payload/types"; import { ArrayField, Field } from "payload/types";
import { Collections } from "../../constants"; import { Collections } from "../../constants";
import { isDefined, isUndefined } from "../../utils/asserts"; import { hasDuplicates, isDefined, isUndefined } from "../../utils/asserts";
import { hasDuplicates } from "../../utils/validation";
import { Cell } from "./Cell"; import { Cell } from "./Cell";
import { RowLabel } from "./RowLabel"; import { RowLabel } from "./RowLabel";
@ -14,7 +13,7 @@ const fieldsNames = {
proofreaders: "proofreaders", proofreaders: "proofreaders",
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
type LocalizedFieldsProps = Omit<ArrayField, "type" | "admin" | "validate"> & { type LocalizedFieldsProps = Omit<ArrayField, "type" | "admin"> & {
admin?: ArrayField["admin"] & { admin?: ArrayField["admin"] & {
useAsTitle?: string; useAsTitle?: string;
hasSourceLanguage?: boolean; hasSourceLanguage?: boolean;
@ -43,7 +42,8 @@ const creditFields: Field = {
type: "row", type: "row",
admin: { admin: {
condition: (_, siblingData) => condition: (_, siblingData) =>
isDefined(siblingData.language) && isDefined(siblingData.sourceLanguage), isDefined(siblingData[fieldsNames.language]) &&
isDefined(siblingData[fieldsNames.sourceLanguage]),
}, },
fields: [ fields: [
{ {
@ -52,18 +52,28 @@ const creditFields: Field = {
type: "relationship", type: "relationship",
relationTo: "recorders", relationTo: "recorders",
hasMany: true, hasMany: true,
hooks: {
beforeChange: [
({ siblingData }) => {
if (siblingData[fieldsNames.language] !== siblingData[fieldsNames.sourceLanguage]) {
delete siblingData[fieldsNames.transcribers];
}
},
],
},
admin: { admin: {
condition: (_, siblingData) => siblingData.language === siblingData.sourceLanguage, condition: (_, siblingData) => siblingData.language === siblingData.sourceLanguage,
width: "50%", width: "50%",
}, },
validate: (count, { siblingData }) => { validate: (count, { siblingData }) => {
if (siblingData.language !== siblingData.sourceLanguage) { if (siblingData[fieldsNames.language] !== siblingData[fieldsNames.sourceLanguage]) {
return true; return true;
} }
if (isDefined(count) && count.length > 0) { if (isDefined(count) && count.length > 0) {
return true; 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", type: "relationship",
relationTo: "recorders", relationTo: "recorders",
hasMany: true, hasMany: true,
hooks: {
beforeChange: [
({ siblingData }) => {
if (siblingData[fieldsNames.language] === siblingData[fieldsNames.sourceLanguage]) {
delete siblingData[fieldsNames.translators];
}
},
],
},
admin: { admin: {
condition: (_, siblingData) => siblingData.language !== siblingData.sourceLanguage, condition: (_, siblingData) =>
siblingData[fieldsNames.language] !== siblingData[fieldsNames.sourceLanguage],
width: "50%", width: "50%",
}, },
validate: (count, { siblingData }) => { validate: (count, { siblingData }) => {
if (siblingData.language === siblingData.sourceLanguage) { if (siblingData[fieldsNames.language] === siblingData[fieldsNames.sourceLanguage]) {
return true; return true;
} }
if (isDefined(count) && count.length > 0) { if (isDefined(count) && count.length > 0) {
return true; 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, fields,
validate,
admin: { useAsTitle, hasSourceLanguage, hasCredits, ...admin } = {}, admin: { useAsTitle, hasSourceLanguage, hasCredits, ...admin } = {},
...otherProps ...otherProps
}: LocalizedFieldsProps): ArrayField => ({ }: LocalizedFieldsProps): ArrayField => ({
@ -111,13 +133,13 @@ export const localizedFields = ({
Cell({ Cell({
cellData: cellData:
cellData?.map((row) => ({ cellData?.map((row) => ({
language: row.language, language: row[fieldsNames.language],
title: isDefined(useAsTitle) ? row[useAsTitle] : undefined, title: isDefined(useAsTitle) ? row[useAsTitle] : undefined,
})) ?? [], })) ?? [],
}), }),
RowLabel: ({ data }) => RowLabel: ({ data }) =>
RowLabel({ RowLabel({
language: data.language, language: data[fieldsNames.language],
title: isDefined(useAsTitle) ? data[useAsTitle] : undefined, title: isDefined(useAsTitle) ? data[useAsTitle] : undefined,
}), }),
}, },
@ -127,11 +149,16 @@ export const localizedFields = ({
const defaultValidation = array(value, options); const defaultValidation = array(value, options);
if (defaultValidation !== true) return defaultValidation; 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; const data = options.data[otherProps.name] as ArrayData;
if (isUndefined(data)) return true; if (isUndefined(data)) return true;
if (typeof data === "number") 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)) { if (hasDuplicates(languages)) {
return `There cannot be multiple ${otherProps.name} with the same ${fieldsNames.language}`; return `There cannot be multiple ${otherProps.name} with the same ${fieldsNames.language}`;
} }

View File

@ -10,6 +10,8 @@ import { Files } from "./collections/Files/Files";
import { Keys } from "./collections/Keys/Keys"; import { Keys } from "./collections/Keys/Keys";
import { Languages } from "./collections/Languages/Languages"; import { Languages } from "./collections/Languages/Languages";
import { LibraryItems } from "./collections/LibraryItems/LibraryItems"; 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 { LibraryItemsThumbnails } from "./collections/LibraryItemsThumbnails/LibraryItemsThumbnails";
import { Posts } from "./collections/Posts/Posts"; import { Posts } from "./collections/Posts/Posts";
import { PostsThumbnails } from "./collections/PostsThumbnails/PostsThumbnails"; import { PostsThumbnails } from "./collections/PostsThumbnails/PostsThumbnails";
@ -48,6 +50,8 @@ export default buildConfig({
WeaponsThumbnails, WeaponsThumbnails,
ContentsThumbnails, ContentsThumbnails,
LibraryItemsThumbnails, LibraryItemsThumbnails,
LibraryItemsScans,
LibraryItemsGallery,
RecordersThumbnails, RecordersThumbnails,
PostsThumbnails, PostsThumbnails,
Files, Files,

View File

@ -36,6 +36,8 @@ export interface Config {
"weapons-thumbnails": WeaponsThumbnail; "weapons-thumbnails": WeaponsThumbnail;
"contents-thumbnails": ContentsThumbnail; "contents-thumbnails": ContentsThumbnail;
"library-items-thumbnails": LibraryItemThumbnail; "library-items-thumbnails": LibraryItemThumbnail;
"library-items-scans": LibraryItemScans;
"library-items-gallery": LibraryItemGallery;
"recorders-thumbnails": RecordersThumbnail; "recorders-thumbnails": RecordersThumbnail;
"posts-thumbnails": PostThumbnail; "posts-thumbnails": PostThumbnail;
files: File; files: File;
@ -50,6 +52,7 @@ export interface Config {
} }
export interface LibraryItem { export interface LibraryItem {
id: string; id: string;
itemType?: "Textual" | "Audio" | "Video" | "Game" | "Other";
slug: string; slug: string;
thumbnail?: string | LibraryItemThumbnail; thumbnail?: string | LibraryItemThumbnail;
pretitle?: string; pretitle?: string;
@ -59,32 +62,72 @@ export interface LibraryItem {
primary: boolean; primary: boolean;
digital: boolean; digital: boolean;
downloadable: boolean; downloadable: boolean;
gallery?: {
image?: string | LibraryItemGallery;
id?: string;
}[];
scans?: { scans?: {
cover?: { cover?: {
front?: string | LibraryItemThumbnail; front?: string | LibraryItemScans;
spine?: string | LibraryItemThumbnail; spine?: string | LibraryItemScans;
back?: string | LibraryItemThumbnail; back?: string | LibraryItemScans;
insideFront?: string | LibraryItemScans;
flapFront?: string | LibraryItemScans;
flapBack?: string | LibraryItemScans;
insideFlapFront?: string | LibraryItemScans;
insideFlapBack?: string | LibraryItemScans;
id?: string; id?: string;
}[]; }[];
dustjacket?: { dustjacket?: {
front?: string | LibraryItemThumbnail; front?: string | LibraryItemScans;
spine?: string | LibraryItemThumbnail; spine?: string | LibraryItemScans;
back?: string | LibraryItemThumbnail; 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; id?: string;
}[]; }[];
obi?: { obi?: {
front?: string | LibraryItemThumbnail; front?: string | LibraryItemScans;
spine?: string | LibraryItemThumbnail; spine?: string | LibraryItemScans;
back?: string | LibraryItemThumbnail; 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; id?: string;
}[]; }[];
pages?: { pages?: {
page: number; page: number;
image: string | LibraryItemThumbnail; image: string | LibraryItemScans;
id?: string; id?: string;
}[]; }[];
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?: { size?: {
width: number; width: number;
height: number; height: number;
@ -96,41 +139,10 @@ export interface LibraryItem {
currency: string | Currency; currency: string | Currency;
id?: string; id?: string;
}[]; }[];
itemType?: "Textual" | "Audio" | "Video" | "Game" | "Other"; urls?: {
textual?: { url: string;
subtype?: id?: string;
| { }[];
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";
}[];
};
contents?: { contents?: {
content: string | Content; content: string | Content;
pageStart?: number; pageStart?: number;
@ -140,7 +152,6 @@ export interface LibraryItem {
note?: string; note?: string;
id?: string; id?: string;
}[]; }[];
releaseDate?: string;
updatedBy: string | Recorder; updatedBy: string | Recorder;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
@ -157,6 +168,96 @@ export interface LibraryItemThumbnail {
width?: number; width?: number;
height?: number; height?: number;
sizes?: { 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?: { og?: {
url?: string; url?: string;
width?: number; width?: number;
@ -183,9 +284,6 @@ export interface LibraryItemThumbnail {
}; };
}; };
} }
export interface Currency {
id: string;
}
export interface Key { export interface Key {
id: string; id: string;
name: string; name: string;
@ -206,23 +304,15 @@ export interface Language {
id: string; id: string;
name: string; name: string;
} }
export interface Currency {
id: string;
}
export interface Content { export interface Content {
id: string; id: string;
slug: string; slug: string;
thumbnail?: string | ContentsThumbnail; thumbnail?: string | ContentsThumbnail;
categories?: categories?: string[] | Key[];
| { type?: string | Key;
value: string;
relationTo: "keys";
}[]
| {
value: Key;
relationTo: "keys";
}[];
type?: {
value: string | Key;
relationTo: "keys";
};
translations: { translations: {
language: string | Language; language: string | Language;
sourceLanguage: string | Language; sourceLanguage: string | Language;
@ -256,6 +346,14 @@ export interface ContentsThumbnail {
width?: number; width?: number;
height?: number; height?: number;
sizes?: { sizes?: {
thumb?: {
url?: string;
width?: number;
height?: number;
mimeType?: string;
filesize?: number;
filename?: string;
};
og?: { og?: {
url?: string; url?: string;
width?: number; width?: number;
@ -303,6 +401,14 @@ export interface RecordersThumbnail {
width?: number; width?: number;
height?: number; height?: number;
sizes?: { sizes?: {
thumb?: {
url?: string;
width?: number;
height?: number;
mimeType?: string;
filesize?: number;
filename?: string;
};
og?: { og?: {
url?: string; url?: string;
width?: number; width?: number;
@ -574,6 +680,14 @@ export interface PostThumbnail {
width?: number; width?: number;
height?: number; height?: number;
sizes?: { sizes?: {
thumb?: {
url?: string;
width?: number;
height?: number;
mimeType?: string;
filesize?: number;
filename?: string;
};
og?: { og?: {
url?: string; url?: string;
width?: number; width?: number;
@ -600,12 +714,25 @@ export interface ChronologyItem {
month?: number; month?: number;
day?: number; day?: number;
}; };
events?: { events: {
translations?: { source?:
| {
value: string | Content;
relationTo: "contents";
}
| {
value: string | LibraryItem;
relationTo: "library-items";
};
translations: {
language: string | Language; language: string | Language;
sourceLanguage: string | Language;
title?: string; title?: string;
description?: string; description?: string;
notes?: string; notes?: string;
transcribers?: string[] | Recorder[];
translators?: string[] | Recorder[];
proofreaders?: string[] | Recorder[];
id?: string; id?: string;
}[]; }[];
id?: string; id?: string;
@ -626,6 +753,7 @@ export interface ChronologyEra {
description?: string; description?: string;
id?: string; id?: string;
}[]; }[];
events?: string[] | ChronologyItem[];
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
@ -669,6 +797,14 @@ export interface WeaponsThumbnail {
width?: number; width?: number;
height?: number; height?: number;
sizes?: { sizes?: {
thumb?: {
url?: string;
width?: number;
height?: number;
mimeType?: string;
filesize?: number;
filename?: string;
};
og?: { og?: {
url?: string; url?: string;
width?: number; width?: number;
@ -685,6 +821,22 @@ export interface WeaponsThumbnail {
filesize?: number; filesize?: number;
filename?: string; 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 { export interface WeaponsGroup {
@ -701,7 +853,7 @@ export interface Video {
id: string; id: string;
uid: string; uid: string;
gone: boolean; gone: boolean;
source: "YouTube" | "NicoNico"; source: "YouTube" | "NicoNico" | "Tumblr";
title: string; title: string;
description?: string; description?: string;
likes?: number; likes?: number;

View File

@ -10,3 +10,17 @@ export const isValidDate = (date: Date): boolean => date instanceof Date && !isN
export const isNotEmpty = (value: string | null | undefined): value is string => export const isNotEmpty = (value: string | null | undefined): value is string =>
isDefined(value) && value.trim().length > 0; 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 = <T>(list: T[]): boolean => list.length !== new Set(list).size;

View File

@ -1,22 +1,12 @@
import { CollectionConfig } from "payload/types"; import { CollectionConfig } from "payload/types";
import { Collections } from "../constants"; import { Collections } from "../constants";
export type BuildCollectionConfig = Omit<CollectionConfig, "slug" | "typescript" | "labels">; export type BuildCollectionConfig = Omit<CollectionConfig, "slug" | "typescript" | "labels"> & {
slug: Collections;
export type GenerationFunctionProps = { labels: { singular: string; plural: string };
uploadDir: string;
}; };
export const buildCollectionConfig = ( export const buildCollectionConfig = (config: BuildCollectionConfig): CollectionConfig => ({
slug: Collections, ...config,
labels: { singular: string; plural: string }, typescript: { interface: config.labels.singular },
generationFunction: (props: GenerationFunctionProps) => BuildCollectionConfig });
): CollectionConfig => {
const uploadDir = `../uploads/${slug}`;
const config = generationFunction({ uploadDir });
return {
...config,
slug,
typescript: { interface: labels.singular },
};
};

View File

@ -0,0 +1,34 @@
import { ImageSize } from "payload/dist/uploads/types";
import { CollectionConfig } from "payload/types";
import { BuildCollectionConfig, buildCollectionConfig } from "./collectionConfig";
type BuildImageCollectionConfig = Omit<BuildCollectionConfig, "upload"> & {
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,
],
},
});

View File

@ -1 +0,0 @@
export const hasDuplicates = <T>(list: T[]): boolean => list.length !== new Set(list).size;

View File

@ -1,10 +1,6 @@
import { CollectionBeforeChangeHook, CollectionConfig, RelationshipField } from "payload/types"; import { CollectionBeforeChangeHook, CollectionConfig, RelationshipField } from "payload/types";
import { Collections } from "../constants"; import { Collections } from "../constants";
import { import { BuildCollectionConfig } from "./collectionConfig";
BuildCollectionConfig,
GenerationFunctionProps,
buildCollectionConfig,
} from "./collectionConfig";
const fields = { updatedBy: "updatedBy" }; const fields = { updatedBy: "updatedBy" };
@ -23,25 +19,17 @@ const updatedByField = (): RelationshipField => ({
type BuildVersionedCollectionConfig = Omit<BuildCollectionConfig, "timestamps" | "versions">; type BuildVersionedCollectionConfig = Omit<BuildCollectionConfig, "timestamps" | "versions">;
export const buildVersionedCollectionConfig = ( export const buildVersionedCollectionConfig = ({
slug: Collections, hooks: { beforeChange, ...otherHooks } = {},
labels: { singular: string; plural: string }, fields,
generationFunction: (props: GenerationFunctionProps) => BuildVersionedCollectionConfig ...otherParams
): CollectionConfig => { }: BuildVersionedCollectionConfig): CollectionConfig => ({
const { ...otherParams,
hooks: { beforeChange, ...otherHooks } = {}, timestamps: true,
fields, versions: { drafts: { autosave: { interval: 2000 } } },
...otherParams hooks: {
} = buildCollectionConfig(slug, labels, generationFunction); ...otherHooks,
beforeChange: [...(beforeChange ?? []), beforeChangeUpdatedBy],
return { },
...otherParams, fields: [...fields, updatedByField()],
timestamps: true, });
versions: { drafts: { autosave: { interval: 2000 } } },
hooks: {
...otherHooks,
beforeChange: [...(beforeChange ?? []), beforeChangeUpdatedBy],
},
fields: [...fields, updatedByField()],
};
};