Added more collections

This commit is contained in:
DrMint 2023-07-20 08:42:24 +02:00
parent 138d7ae571
commit 84f0b20999
30 changed files with 1350 additions and 114 deletions

11
package-lock.json generated
View File

@ -13,7 +13,8 @@
"dotenv": "^8.2.0",
"express": "^4.17.1",
"iso-639-1": "^2.1.15",
"payload": "^1.11.1"
"payload": "^1.11.1",
"slugify": "^1.6.6"
},
"devDependencies": {
"@types/express": "^4.17.9",
@ -8504,6 +8505,14 @@
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.6.tgz",
"integrity": "sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA=="
},
"node_modules/slugify": {
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz",
"integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",

View File

@ -22,7 +22,8 @@
"dotenv": "^8.2.0",
"express": "^4.17.1",
"iso-639-1": "^2.1.15",
"payload": "^1.11.1"
"payload": "^1.11.1",
"slugify": "^1.6.6"
},
"devDependencies": {
"@types/express": "^4.17.9",

View File

@ -1,48 +0,0 @@
import { CollectionConfig } from "payload/types";
import { localizedFields } from "../../elements/translatedFields/translatedFields";
const fields = {
id: "id",
translations: "translations",
name: "name",
short: "short",
} as const satisfies Record<string, string>;
const labels = {
singular: "Category",
plural: "Categories",
} as const satisfies { singular: string; plural: string };
export const Categories: CollectionConfig = {
slug: labels.plural,
labels,
typescript: { interface: labels.singular },
defaultSort: fields.id,
admin: {
useAsTitle: fields.id,
defaultColumns: [fields.id, fields.translations],
},
timestamps: false,
fields: [
{
name: fields.id,
type: "text",
},
localizedFields({
name: fields.translations,
interfaceName: "CategoryTranslations",
admin: {
useAsTitle: fields.name,
},
fields: [
{
type: "row",
fields: [
{ name: fields.name, type: "text", required: true },
{ name: fields.short, type: "text" },
],
},
],
}),
],
};

View File

@ -0,0 +1,169 @@
import { Block, BlockField } from "payload/types";
import { cueBlock } from "./cueBlock";
import { textBlock } from "./textBlock";
import { transcriptBlock } from "./transcriptBlock";
import { lineBlock } from "./lineBlock";
const INITIAL_DEPTH = 1;
const MAX_DEPTH = 4;
enum BlockName {
Text = "Text",
Section = "Section",
Tabs = "Tabs",
Tab = "Tab",
Columns = "Columns",
Column = "Column",
Transcript = "Transcript",
Collapsible = "Collapsible",
Accordion = "Accordion",
Line = "Line",
Cue = "Cue",
}
const rootBlocksNames: BlockName[] = [
BlockName.Section,
BlockName.Collapsible,
BlockName.Columns,
BlockName.Tabs,
BlockName.Accordion,
BlockName.Text,
BlockName.Transcript,
];
const recursiveBlocks: BlockName[] = [
BlockName.Section,
BlockName.Collapsible,
BlockName.Accordion,
BlockName.Tabs,
BlockName.Tab,
BlockName.Column,
BlockName.Columns,
];
const blocksChildren: Record<BlockName, BlockName[]> = {
Tabs: [BlockName.Tab],
Columns: [BlockName.Column],
Section: rootBlocksNames,
Collapsible: rootBlocksNames,
Tab: rootBlocksNames,
Column: rootBlocksNames,
Accordion: [BlockName.Collapsible],
Text: [],
Transcript: [BlockName.Line, BlockName.Cue],
Cue: [],
Line: [],
};
export type RecursiveBlock = Omit<Block, "fields"> & {
fields: Omit<BlockField, "blocks" | "type"> & {
newDepth: (currentDepth: number) => number;
blocks: BlockName[];
};
};
// TODO: Check for loops in the block graph instead of manually defining recursive blocks
const isNotRecursiveBlock = (name: BlockName): boolean => !recursiveBlocks.includes(name);
const implementationForRecursiveBlocks = (
currentDepth: number,
{ slug, interfaceName, labels, fields: { newDepth, blocks, ...fieldsProps } }: RecursiveBlock
): Block => ({
slug: [slug, currentDepth].join("_"),
interfaceName: [interfaceName, currentDepth].join("_"),
labels,
fields: [
{
...fieldsProps,
type: "blocks",
blocks: blocks
.filter((block) => {
if (currentDepth < MAX_DEPTH) return true;
if (blocks.filter(isNotRecursiveBlock).length === 0) return true;
return isNotRecursiveBlock(block);
})
.map((block) => implementations[block](newDepth(currentDepth))),
},
],
});
const implementations: Record<BlockName, (currentDepth: number) => Block> = {
Cue: () => cueBlock,
Text: () => textBlock,
Transcript: () => transcriptBlock,
Line: () => lineBlock,
Section: (currentDepth) =>
implementationForRecursiveBlocks(currentDepth, {
slug: "section",
interfaceName: "Section",
labels: { singular: "Section", plural: "Sections" },
fields: {
name: "content",
newDepth: (depth) => depth + 1,
blocks: blocksChildren.Section,
},
}),
Accordion: (currentDepth) =>
implementationForRecursiveBlocks(currentDepth, {
slug: "accordion",
interfaceName: "Accordion",
labels: { singular: "Accordion", plural: "Accordions" },
fields: {
name: "content",
newDepth: (depth) => depth + 1,
blocks: blocksChildren.Accordion,
},
}),
Collapsible: (currentDepth) =>
implementationForRecursiveBlocks(currentDepth, {
slug: "collapsible",
interfaceName: "Collapsible",
labels: { singular: "Collapsible", plural: "Collapsibles" },
fields: {
name: "content",
newDepth: (depth) => depth + 1,
blocks: blocksChildren.Collapsible,
},
}),
Tabs: (currentDepth) =>
implementationForRecursiveBlocks(currentDepth, {
slug: "tabs",
interfaceName: "Tabs",
labels: { singular: "Tabs", plural: "Tabs" },
fields: { name: "tabs", newDepth: (depth) => depth, blocks: blocksChildren.Tabs },
}),
Tab: (currentDepth) =>
implementationForRecursiveBlocks(currentDepth, {
slug: "tab",
interfaceName: "Tab",
labels: { singular: "Tab", plural: "Tabs" },
fields: {
name: "content",
newDepth: (depth) => depth + 1,
blocks: blocksChildren.Tab,
},
}),
Columns: (currentDepth) =>
implementationForRecursiveBlocks(currentDepth, {
slug: "columns",
interfaceName: "Columns",
labels: { singular: "Columns", plural: "Columns" },
fields: {
name: "columns",
newDepth: (depth) => depth,
blocks: blocksChildren.Columns,
},
}),
Column: (currentDepth) =>
implementationForRecursiveBlocks(currentDepth, {
slug: "column",
interfaceName: "Column",
labels: { singular: "Column", plural: "Columns" },
fields: { name: "content", newDepth: (depth) => depth + 1, blocks: blocksChildren.Column },
}),
};
export const rootBlocks: Block[] = rootBlocksNames
.filter((block) => block in implementations)
.map((block) => implementations[block](INITIAL_DEPTH));

View File

@ -0,0 +1,19 @@
import { Block } from "payload/types";
export const cueBlock: Block = {
slug: "cueBlock",
interfaceName: "CueBlock",
labels: { singular: "Cue", plural: "Cues" },
fields: [
{
name: "content",
label: false,
type: "textarea",
required: true,
admin: {
description:
"Parenthesis will automatically be added around cues. You don't have to include them here.",
},
},
],
};

View File

@ -0,0 +1,20 @@
import { Block } from "payload/types";
export const lineBlock: Block = {
slug: "lineBlock",
interfaceName: "LineBlock",
labels: { singular: "Line", plural: "Lines" },
fields: [
{
name: "content",
label: false,
type: "richText",
required: true,
admin: {
hideGutter: true,
elements: [],
leaves: ["bold", "italic", "underline", "strikethrough", "code"],
},
},
],
};

View File

@ -0,0 +1,19 @@
import { Block } from "payload/types";
export const textBlock: Block = {
slug: "textBlock",
interfaceName: "TextBlock",
labels: { singular: "Text", plural: "Texts" },
fields: [
{
name: "content",
type: "richText",
label: false,
required: true,
admin: {
hideGutter: true,
elements: ["ul", "ol", "indent", "link", "relationship", "upload", "blockquote"],
},
},
],
};

View File

@ -0,0 +1,19 @@
import { Block } from "payload/types";
import { lineBlock } from "./lineBlock";
import { cueBlock } from "./cueBlock";
export const transcriptBlock: Block = {
slug: "transcriptBlock",
interfaceName: "TranscriptBlock",
labels: { singular: "Transcript", plural: "Transcripts" },
fields: [
{
name: "lines",
type: "blocks",
required: true,
minRows: 1,
admin: { initCollapsed: true },
blocks: [lineBlock, cueBlock],
},
],
};

View File

@ -0,0 +1,208 @@
import { CollectionConfig } from "payload/types";
import { collectionSlug } from "../../utils/string";
import { CollectionGroup, FileTypes, TagsTypes } from "../../constants";
import { slugField } from "../../fields/slugField/slugField";
import { imageField } from "../../fields/imageField/imageField";
import { Tags } from "../Tags/Tags";
import { localizedFields } from "../../fields/translatedFields/translatedFields";
import { Recorders } from "../Recorders/Recorders";
import { isDefined } from "../../utils/asserts";
import { fileField } from "../../fields/fileField/fileField";
import { rootBlocks } from "./Blocks/blocks";
const fields = {
slug: "slug",
thumbnail: "thumbnail",
categories: "categories",
type: "type",
translations: "translations",
pretitle: "pretitle",
title: "title",
subtitle: "subtitle",
summary: "summary",
textContent: "textContent",
textTranscribers: "textTranscribers",
textTranslators: "textTranslators",
textProofreaders: "textProofreaders",
textNotes: "textNotes",
video: "video",
videoNotes: "videoNotes",
audio: "audio",
audioNotes: "videoNotes",
} as const satisfies Record<string, string>;
const labels = {
singular: "Content",
plural: "Contents",
} as const satisfies { singular: string; plural: string };
export const Contents: CollectionConfig = {
slug: collectionSlug(labels.plural),
labels,
typescript: { interface: labels.singular },
defaultSort: fields.slug,
admin: {
useAsTitle: fields.slug,
defaultColumns: [fields.slug, fields.thumbnail, fields.categories],
group: CollectionGroup.Collections,
preview: (doc) => `https://accords-library.com/contents/${doc.slug}`,
},
timestamps: true,
versions: { drafts: true },
fields: [
{
type: "row",
fields: [
slugField({ name: fields.slug, admin: { width: "50%" } }),
imageField({ name: fields.thumbnail, admin: { width: "50%" } }),
],
},
{
name: "testing",
type: "blocks",
admin: { initCollapsed: true },
blocks: rootBlocks,
},
{
type: "row",
fields: [
{
name: fields.categories,
type: "relationship",
relationTo: [Tags.slug],
filterOptions: { type: { equals: TagsTypes.Categories } },
hasMany: true,
admin: { allowCreate: false, width: "50%" },
},
{
name: fields.type,
type: "relationship",
relationTo: [Tags.slug],
filterOptions: { type: { equals: TagsTypes.Contents } },
admin: { allowCreate: false, width: "50%" },
},
],
},
localizedFields({
name: fields.translations,
admin: { useAsTitle: fields.title, hasSourceLanguage: true },
required: true,
minRows: 1,
fields: [
{
type: "row",
fields: [
{ name: fields.pretitle, type: "text" },
{ name: fields.title, type: "text", required: true },
{ name: fields.subtitle, type: "text" },
],
},
{ name: fields.summary, type: "textarea" },
{
type: "tabs",
admin: {
condition: (_, siblingData) =>
isDefined(siblingData.language) && isDefined(siblingData.sourceLanguage),
},
tabs: [
{
label: "Text",
fields: [
{
type: "row",
fields: [
{
name: fields.textTranscribers,
label: "Transcribers",
type: "relationship",
relationTo: Recorders.slug,
hasMany: true,
admin: {
condition: (_, siblingData) =>
siblingData.language === siblingData.sourceLanguage,
width: "50%",
},
},
{
name: fields.textTranslators,
label: "Translators",
type: "relationship",
relationTo: Recorders.slug,
hasMany: true,
admin: {
condition: (_, siblingData) =>
siblingData.language !== siblingData.sourceLanguage,
width: "50%",
},
},
{
name: fields.textProofreaders,
label: "Proofreaders",
type: "relationship",
relationTo: Recorders.slug,
hasMany: true,
admin: { width: "50%" },
},
],
},
{
name: fields.textContent,
label: "Content",
type: "richText",
admin: { hideGutter: true },
},
{
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

@ -0,0 +1,38 @@
import { CollectionConfig } from "payload/types";
import { CollectionGroup, FileTypes } from "../../constants";
import { collectionSlug } from "../../utils/string";
const fields = {
filename: "filename",
type: "type",
} as const satisfies Record<string, string>;
const labels = {
singular: "File",
plural: "Files",
} as const satisfies { singular: string; plural: string };
export const Files: CollectionConfig = {
slug: collectionSlug(labels.plural),
labels,
typescript: { interface: labels.singular },
defaultSort: fields.filename,
admin: {
useAsTitle: fields.filename,
group: CollectionGroup.Media,
},
fields: [
{
name: fields.filename,
required: true,
type: "text",
},
{
name: fields.type,
type: "select",
required: true,
options: Object.entries(FileTypes).map(([value, label]) => ({ label, value })),
},
],
};

View File

@ -1,4 +1,6 @@
import { CollectionConfig } from "payload/types";
import { CollectionGroup } from "../../constants";
import { collectionSlug } from "../../utils/string";
const fields = {
filename: "filename",
@ -13,13 +15,13 @@ const labels = {
} as const satisfies { singular: string; plural: string };
export const Images: CollectionConfig = {
slug: labels.plural,
slug: collectionSlug(labels.plural),
labels,
typescript: { interface: labels.singular },
defaultSort: fields.filename,
admin: {
useAsTitle: fields.filename,
group: "Media",
group: CollectionGroup.Media,
},
upload: {

View File

@ -1,4 +1,6 @@
import { CollectionConfig } from "payload/types";
import { CollectionGroup } from "../constants";
import { collectionSlug } from "../utils/string";
const fields = {
id: "id",
@ -11,13 +13,14 @@ const labels = {
} as const satisfies { singular: string; plural: string };
export const Languages: CollectionConfig = {
slug: labels.plural,
slug: collectionSlug(labels.plural),
labels,
typescript: { interface: labels.singular },
defaultSort: fields.name,
admin: {
useAsTitle: fields.name,
defaultColumns: [fields.name, fields.id],
group: CollectionGroup.Meta,
},
timestamps: false,
fields: [

View File

@ -0,0 +1,148 @@
import { CollectionConfig } from "payload/types";
import { CollectionGroup } from "../../constants";
import { slugField } from "../../fields/slugField/slugField";
import { imageField } from "../../fields/imageField/imageField";
import { collectionSlug } from "../../utils/string";
import { isDefined, isUndefined } from "../../utils/asserts";
const fields = {
status: "status",
slug: "slug",
thumbnail: "thumbnail",
pretitle: "pretitle",
title: "title",
subtitle: "subtitle",
rootItem: "rootItem",
primary: "primary",
digital: "digital",
downloadable: "downloadable",
size: "size",
width: "width",
height: "height",
thickness: "thickness",
} as const satisfies Record<string, string>;
const labels = {
singular: "Library Item",
plural: "Library Items",
} as const satisfies { singular: string; plural: string };
const validateSizeValue = (value?: number) => {
if (isDefined(value) && value <= 0) return "This value must be greater than 0";
return true;
};
const validateRequiredSizeValue = (value?: number) => {
if (isUndefined(value)) return "This field is required.";
if (value <= 0) return "This value must be greater than 0.";
return true;
};
export const LibraryItems: CollectionConfig = {
slug: collectionSlug(labels.plural),
labels,
typescript: { interface: labels.singular },
defaultSort: fields.slug,
admin: {
useAsTitle: fields.slug,
defaultColumns: [fields.slug, fields.thumbnail, fields.status],
group: CollectionGroup.Collections,
preview: (doc) => `https://accords-library.com/library/${doc.slug}`,
},
timestamps: true,
versions: { drafts: true },
fields: [
{
type: "row",
fields: [
slugField({ name: fields.slug, admin: { width: "50%" } }),
imageField({ name: fields.thumbnail, admin: { width: "50%" } }),
],
},
{
type: "row",
fields: [
{ name: fields.pretitle, type: "text" },
{ name: fields.title, type: "text", required: true },
{ name: fields.subtitle, type: "text" },
],
},
{
type: "row",
fields: [
{
name: fields.rootItem,
type: "checkbox",
required: true,
defaultValue: true,
admin: {
description: "Only items that can be sold separetely should be root items.",
width: "25%",
},
},
{
name: fields.primary,
type: "checkbox",
required: true,
defaultValue: true,
admin: {
description:
"A primary item is an official item that focuses primarly on one or more of our Categories.",
width: "25%",
},
},
{
name: fields.digital,
type: "checkbox",
required: true,
defaultValue: false,
admin: {
description:
"The item is the digital version of another item, or the item is sold only digitally.",
width: "25%",
},
},
{
name: fields.downloadable,
type: "checkbox",
required: true,
defaultValue: false,
admin: {
description: "Are the scans available for download?",
width: "25%",
},
},
],
},
{
name: "size",
type: "group",
admin: { condition: (data) => !data.digital },
fields: [
{
type: "row",
fields: [
{
name: fields.width,
type: "number",
validate: validateRequiredSizeValue,
admin: { step: 1, width: "33%", description: "in mm." },
},
{
name: fields.height,
type: "number",
validate: validateRequiredSizeValue,
admin: { step: 1, width: "33%", description: "in mm." },
},
{
name: fields.thickness,
type: "number",
validate: validateSizeValue,
admin: { step: 1, width: "33%", description: "in mm." },
},
],
},
],
},
],
};

View File

@ -0,0 +1,142 @@
import { CollectionConfig } from "payload/types";
import { slugField } from "../../fields/slugField/slugField";
import { imageField } from "../../fields/imageField/imageField";
import { CollectionGroup, TagsTypes } from "../../constants";
import { Recorders } from "../Recorders/Recorders";
import { localizedFields } from "../../fields/translatedFields/translatedFields";
import { isDefined, isUndefined } from "../../utils/asserts";
import { removeTranslatorsForTranscripts } from "./hooks/beforeValidate";
import { Tags } from "../Tags/Tags";
import { collectionSlug } from "../../utils/string";
const fields = {
slug: "slug",
thumbnail: "thumbnail",
categories: "categories",
authors: "authors",
publishedDate: "publishedDate",
translations: "translations",
sourceLanguage: "sourceLanguage",
title: "title",
summary: "summary",
content: "content",
translators: "translators",
proofreaders: "proofreaders",
} as const satisfies Record<string, string>;
const labels = {
singular: "Post",
plural: "Posts",
} as const satisfies { singular: string; plural: string };
export const Posts: CollectionConfig = {
slug: collectionSlug(labels.plural),
labels,
typescript: { interface: labels.singular },
defaultSort: fields.slug,
admin: {
useAsTitle: fields.slug,
defaultColumns: [fields.slug, fields.thumbnail, fields.categories],
group: CollectionGroup.Collections,
preview: (doc) => `https://accords-library.com/news/${doc.slug}`,
},
hooks: {
beforeValidate: [removeTranslatorsForTranscripts],
},
timestamps: true,
versions: { drafts: true },
fields: [
{
type: "row",
fields: [
slugField({ name: fields.slug, admin: { width: "50%" } }),
imageField({ name: fields.thumbnail, admin: { width: "50%" } }),
],
},
{
type: "row",
fields: [
{
name: fields.authors,
type: "relationship",
relationTo: [Recorders.slug],
required: true,
minRows: 1,
hasMany: true,
admin: { width: "50%" },
},
{
name: fields.categories,
type: "relationship",
relationTo: [Tags.slug],
filterOptions: { type: { equals: TagsTypes.Categories } },
hasMany: true,
admin: { allowCreate: false, width: "50%" },
},
],
},
localizedFields({
name: fields.translations,
admin: { useAsTitle: fields.title, hasSourceLanguage: true },
required: true,
minRows: 1,
fields: [
{ name: fields.title, type: "text", required: true },
{ name: fields.summary, type: "textarea" },
{
type: "row",
fields: [
{
name: fields.translators,
type: "relationship",
relationTo: Recorders.slug,
hasMany: true,
admin: {
condition: (_, siblingData) => {
if (
isUndefined(siblingData.language) ||
isUndefined(siblingData.sourceLanguage)
) {
return false;
}
return siblingData.language !== siblingData.sourceLanguage;
},
width: "50%",
},
validate: (translators, { siblingData }) => {
if (isUndefined(siblingData.language) || isUndefined(siblingData.sourceLanguage)) {
return true;
}
if (siblingData.language === siblingData.sourceLanguage) {
return true;
}
if (isDefined(translators) && translators.length > 0) {
return true;
}
return "This field is required when the language is different from the source language.";
},
},
{
name: fields.proofreaders,
type: "relationship",
relationTo: Recorders.slug,
hasMany: true,
admin: { width: "50%" },
},
],
},
{ name: fields.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,
},
],
};

View File

@ -0,0 +1,14 @@
import { CollectionBeforeValidateHook } from "payload/types";
import { Post } from "../../../types/collections";
export const removeTranslatorsForTranscripts: CollectionBeforeValidateHook<Post> = async ({
data: { translations, ...data },
}) => ({
...data,
translations: translations.map(({ translators, ...translation }) => {
if (translation.language === translation.sourceLanguage) {
return { ...translation, translators: [] };
}
return { ...translation, translators };
}),
});

View File

@ -1,9 +1,12 @@
import { CollectionConfig } from "payload/types";
import { localizedFields } from "../../elements/translatedFields/translatedFields";
import { localizedFields } from "../../fields/translatedFields/translatedFields";
import { Languages } from "../Languages";
import { Images } from "../Images/Images";
import { ImageCell } from "../Images/components/ImageCell";
import { Cell } from "../../fields/imageField/Cell";
import { beforeDuplicate } from "./hooks/beforeDuplicate";
import { imageField } from "../../fields/imageField/imageField";
import { CollectionGroup } from "../../constants";
import { collectionSlug } from "../../utils/string";
const fields = {
username: "username",
@ -20,7 +23,7 @@ const labels = {
} as const satisfies { singular: string; plural: string };
export const Recorders: CollectionConfig = {
slug: labels.plural,
slug: collectionSlug(labels.plural),
labels,
typescript: { interface: labels.singular },
defaultSort: fields.username,
@ -30,22 +33,14 @@ export const Recorders: CollectionConfig = {
description:
"Recorders are contributors of the Accord's Library project. Create a Recorder here to be able to credit them in other collections",
defaultColumns: [fields.username, fields.anonymize, fields.biographies, fields.languages],
group: CollectionGroup.Meta,
},
timestamps: false,
fields: [
{
type: "row",
fields: [
{
name: fields.avatar,
type: "upload",
relationTo: Images.slug,
admin: {
components: {
Cell: ImageCell,
},
},
},
imageField({ name: fields.avatar }),
{
name: fields.username,
type: "text",

View File

@ -0,0 +1,48 @@
import { CollectionConfig } from "payload/types";
import { slugField } from "../../fields/slugField/slugField";
import { CollectionGroup, TagsTypes } from "../../constants";
import { localizedFields } from "../../fields/translatedFields/translatedFields";
import { collectionSlug } from "../../utils/string";
const fields = {
slug: "slug",
translations: "translations",
type: "type",
name: "name",
} as const satisfies Record<string, string>;
const labels = {
singular: "Tag",
plural: "Tags",
} as const satisfies { singular: string; plural: string };
export const Tags: CollectionConfig = {
slug: collectionSlug(labels.plural),
labels,
typescript: { interface: labels.singular },
defaultSort: fields.slug,
admin: {
useAsTitle: fields.slug,
defaultColumns: [fields.slug, fields.type, fields.translations],
group: CollectionGroup.Meta,
},
timestamps: false,
versions: false,
fields: [
slugField({ name: fields.slug }),
{
name: fields.type,
type: "select",
required: true,
options: Object.entries(TagsTypes).map(([value, label]) => ({ label, value })),
},
localizedFields({
name: fields.translations,
interfaceName: "CategoryTranslations",
admin: {
useAsTitle: fields.name,
},
fields: [{ name: fields.name, type: "text", required: true }],
}),
],
};

View File

@ -1,4 +1,6 @@
import { CollectionConfig } from "payload/types";
import { CollectionGroup } from "../constants";
import { collectionSlug } from "../utils/string";
const fields = {
email: "email",
@ -10,7 +12,7 @@ const labels = {
} as const satisfies { singular: string; plural: string };
export const Users: CollectionConfig = {
slug: labels.plural,
slug: collectionSlug(labels.plural),
auth: true,
labels,
typescript: { interface: labels.singular },
@ -18,7 +20,7 @@ export const Users: CollectionConfig = {
admin: {
useAsTitle: fields.email,
defaultColumns: [fields.email],
group: "Administration",
group: CollectionGroup.Administration,
},
timestamps: false,
fields: [],

25
src/constants.ts Normal file
View File

@ -0,0 +1,25 @@
export enum CollectionGroup {
Collections = "Collections",
Media = "Media",
Meta = "Meta",
Administration = "Administration",
}
export enum TagsTypes {
Contents = "Contents",
LibraryAudio = "Library / Audio",
LibraryVideo = "Library / Video",
LibraryTextual = "Library / Textual",
LibraryGroup = "Library / Group",
Library = "Library",
Weapons = "Weapons",
GamePlatforms = "Game Platforms",
Categories = "Categories",
}
export enum FileTypes {
LibraryScans = "Library / Scans",
LibrarySoundtracks = "Library / Soundtracks",
ContentVideo = "Content / Video",
ContentAudio = "Content / Audio",
}

View File

@ -0,0 +1,10 @@
import { RelationshipField, UploadField } from "payload/types";
import { Files } from "../../collections/Files/Files";
type Props = Omit<UploadField, "type" | "relationTo">;
export const fileField = (props: Props): RelationshipField => ({
...props,
type: "relationship",
relationTo: Files.slug,
});

View File

@ -2,7 +2,7 @@ import { Props } from "payload/components/views/Cell";
import { useState, useEffect } from "react";
import React from "react";
export const ImageCell = ({ cellData, field }: Props): JSX.Element => {
export const Cell = ({ cellData, field }: Props): JSX.Element => {
const [imageURL, setImageURL] = useState<string>();
useEffect(() => {
const fetchUrl = async () => {

View File

@ -0,0 +1,17 @@
import { UploadField } from "payload/types";
import { Images } from "../../collections/Images/Images";
import { Cell } from "./Cell";
type Props = Omit<UploadField, "type" | "relationTo">;
export const imageField = ({ admin, ...otherProps }: Props): UploadField => ({
...otherProps,
type: "upload",
relationTo: Images.slug,
admin: {
components: {
Cell,
},
...admin,
},
});

View File

@ -0,0 +1,23 @@
import { TextField } from "payload/types";
import { isUndefined } from "../../utils/asserts";
type Props = Omit<TextField, "type">;
const validateSlug = (value?: string) => {
if (isUndefined(value) || value === "") return "This field is required.";
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) return "This is not a valid slug.";
return true;
};
export const slugField = ({ admin, ...otherProps }: Props): TextField => ({
...otherProps,
type: "text",
required: true,
unique: true,
validate: validateSlug,
admin: {
description:
"A slug must only include lowercase letters and digits. Instead of spaces you can use dashes.",
...admin,
},
});

View File

@ -1,21 +1,40 @@
import { ArrayField } from "payload/types";
import { ArrayField, Field } from "payload/types";
import { hasDuplicates } from "../../utils/validation";
import { isDefined, isUndefined } from "../../utils/asserts";
import { Languages } from "../../collections/Languages";
import { RowLabel } from "./RowLabel";
import { Cell } from "./Cell";
const LANGUAGE_FIELD_NAME = "language";
const fieldsNames = {
language: "language",
sourceLanguage: "sourceLanguage",
} as const satisfies Record<string, string>;
type LocalizedFieldsProps = Omit<ArrayField, "type" | "admin"> & {
admin?: ArrayField["admin"] & { useAsTitle?: string };
admin?: ArrayField["admin"] & { useAsTitle?: string; hasSourceLanguage?: boolean };
};
type ArrayData = { [fieldsNames.language]?: string }[] | number | undefined;
const languageField: Field = {
name: fieldsNames.language,
type: "relationship",
relationTo: Languages.slug,
required: true,
admin: { allowCreate: false, width: "50%" },
};
const sourceLanguageField: Field = {
name: fieldsNames.sourceLanguage,
type: "relationship",
relationTo: Languages.slug,
required: true,
admin: { allowCreate: false, width: "50%" },
};
type ArrayData = { [LANGUAGE_FIELD_NAME]?: string }[] | number | undefined;
export const localizedFields = ({
fields,
validate,
admin: { useAsTitle, ...admin },
admin: { useAsTitle, hasSourceLanguage, ...admin },
...otherProps
}: LocalizedFieldsProps): ArrayField => ({
...otherProps,
@ -45,19 +64,15 @@ export const localizedFields = ({
const languages = data.map((biography) => biography.language);
if (hasDuplicates(languages)) {
return `There cannot be multiple ${otherProps.name} with the same ${LANGUAGE_FIELD_NAME}`;
return `There cannot be multiple ${otherProps.name} with the same ${fieldsNames.language}`;
}
return isDefined(validate) ? validate(value, options) : true;
},
fields: [
{
name: LANGUAGE_FIELD_NAME,
type: "relationship",
relationTo: Languages.slug,
required: true,
admin: { allowCreate: false },
},
hasSourceLanguage
? { type: "row", fields: [languageField, sourceLanguageField] }
: languageField,
...fields,
],
});

View File

@ -4,14 +4,18 @@ import { Users } from "./collections/Users";
import { Languages } from "./collections/Languages";
import { Recorders } from "./collections/Recorders/Recorders";
import { Images } from "./collections/Images/Images";
import { Categories } from "./collections/Categories/Categories";
import { Posts } from "./collections/Posts/Posts";
import { Tags } from "./collections/Tags/Tags";
import { LibraryItems } from "./collections/LibraryItems/LibraryItems";
import { Contents } from "./collections/Contents/Contents";
import { Files } from "./collections/Files/Files";
export default buildConfig({
serverURL: "http://localhost:3000",
admin: {
user: Users.slug,
},
collections: [Users, Languages, Recorders, Images, Categories],
collections: [LibraryItems, Contents, Posts, Images, Files, Languages, Recorders, Tags, Users],
globals: [],
telemetry: false,
typescript: {

View File

@ -6,6 +6,11 @@
* and re-run `payload generate:types` to regenerate this file.
*/
export type CategoryTranslations = {
language: string | Language;
name: string;
id?: string;
}[];
export type RecorderBiographies = {
language: string | Language;
biography?: string;
@ -14,35 +19,37 @@ export type RecorderBiographies = {
export interface Config {
collections: {
Users: User;
Languages: Language;
Recorders: Recorder;
Images: Image;
'library-items': LibraryItem;
contents: Content;
posts: Post;
images: Image;
files: File;
languages: Language;
recorders: Recorder;
tags: Tag;
users: User;
};
globals: {};
}
export interface User {
export interface LibraryItem {
id: string;
email: string;
resetPasswordToken?: string;
resetPasswordExpiration?: string;
salt?: string;
hash?: string;
loginAttempts?: number;
lockUntil?: string;
password?: string;
}
export interface Language {
id: string;
name: string;
}
export interface Recorder {
id: string;
avatar?: string | Image;
username: string;
anonymize: boolean;
languages?: string[] | Language[];
biographies?: RecorderBiographies;
slug: string;
thumbnail?: string | Image;
pretitle?: string;
title: string;
subtitle?: string;
rootItem: boolean;
primary: boolean;
digital: boolean;
downloadable: boolean;
size?: {
width?: number;
height?: number;
thickness?: number;
};
updatedAt: string;
createdAt: string;
_status?: 'draft' | 'published';
}
export interface Image {
id: string;
@ -56,3 +63,330 @@ export interface Image {
width?: number;
height?: number;
}
export interface Content {
id: string;
slug: string;
thumbnail?: string | Image;
testing?: (Section_1 | Collapsible_1 | Columns_1 | Tabs_1 | Accordion_1 | TextBlock | TranscriptBlock)[];
categories?:
| {
value: string;
relationTo: 'tags';
}[]
| {
value: Tag;
relationTo: 'tags';
}[];
type?: {
value: string | Tag;
relationTo: 'tags';
};
translations: {
language: string | Language;
sourceLanguage: string | Language;
pretitle?: string;
title: string;
subtitle?: string;
summary?: string;
textTranscribers?: string[] | Recorder[];
textTranslators?: string[] | Recorder[];
textProofreaders?: string[] | Recorder[];
textContent?: {
[k: string]: unknown;
}[];
textNotes?: string;
video?: string | File;
videoNotes?: string;
audio?: string | File;
id?: string;
}[];
updatedAt: string;
createdAt: string;
_status?: 'draft' | 'published';
}
export interface Section_1 {
content?: (Section_2 | Collapsible_2 | Columns_2 | Tabs_2 | Accordion_2 | TextBlock | TranscriptBlock)[];
id?: string;
blockName?: string;
blockType: 'section_1';
}
export interface Section_2 {
content?: (Section_3 | Collapsible_3 | Columns_3 | Tabs_3 | Accordion_3 | TextBlock | TranscriptBlock)[];
id?: string;
blockName?: string;
blockType: 'section_2';
}
export interface Section_3 {
content?: (Section_4 | Collapsible_4 | Columns_4 | Tabs_4 | Accordion_4 | TextBlock | TranscriptBlock)[];
id?: string;
blockName?: string;
blockType: 'section_3';
}
export interface Section_4 {
content?: (TextBlock | TranscriptBlock)[];
id?: string;
blockName?: string;
blockType: 'section_4';
}
export interface TextBlock {
content: {
[k: string]: unknown;
}[];
id?: string;
blockName?: string;
blockType: 'textBlock';
}
export interface TranscriptBlock {
lines: (LineBlock | CueBlock)[];
id?: string;
blockName?: string;
blockType: 'transcriptBlock';
}
export interface LineBlock {
content: {
[k: string]: unknown;
}[];
id?: string;
blockName?: string;
blockType: 'lineBlock';
}
export interface CueBlock {
content: string;
id?: string;
blockName?: string;
blockType: 'cueBlock';
}
export interface Collapsible_4 {
content?: (TextBlock | TranscriptBlock)[];
id?: string;
blockName?: string;
blockType: 'collapsible_4';
}
export interface Columns_4 {
columns?: Column_4[];
id?: string;
blockName?: string;
blockType: 'columns_4';
}
export interface Column_4 {
content?: (TextBlock | TranscriptBlock)[];
id?: string;
blockName?: string;
blockType: 'column_4';
}
export interface Tabs_4 {
tabs?: Tab_4[];
id?: string;
blockName?: string;
blockType: 'tabs_4';
}
export interface Tab_4 {
content?: (TextBlock | TranscriptBlock)[];
id?: string;
blockName?: string;
blockType: 'tab_4';
}
export interface Accordion_4 {
content?: Collapsible_5[];
id?: string;
blockName?: string;
blockType: 'accordion_4';
}
export interface Collapsible_5 {
content?: (TextBlock | TranscriptBlock)[];
id?: string;
blockName?: string;
blockType: 'collapsible_5';
}
export interface Collapsible_3 {
content?: (Section_4 | Collapsible_4 | Columns_4 | Tabs_4 | Accordion_4 | TextBlock | TranscriptBlock)[];
id?: string;
blockName?: string;
blockType: 'collapsible_3';
}
export interface Columns_3 {
columns?: Column_3[];
id?: string;
blockName?: string;
blockType: 'columns_3';
}
export interface Column_3 {
content?: (Section_4 | Collapsible_4 | Columns_4 | Tabs_4 | Accordion_4 | TextBlock | TranscriptBlock)[];
id?: string;
blockName?: string;
blockType: 'column_3';
}
export interface Tabs_3 {
tabs?: Tab_3[];
id?: string;
blockName?: string;
blockType: 'tabs_3';
}
export interface Tab_3 {
content?: (Section_4 | Collapsible_4 | Columns_4 | Tabs_4 | Accordion_4 | TextBlock | TranscriptBlock)[];
id?: string;
blockName?: string;
blockType: 'tab_3';
}
export interface Accordion_3 {
content?: Collapsible_4[];
id?: string;
blockName?: string;
blockType: 'accordion_3';
}
export interface Collapsible_2 {
content?: (Section_3 | Collapsible_3 | Columns_3 | Tabs_3 | Accordion_3 | TextBlock | TranscriptBlock)[];
id?: string;
blockName?: string;
blockType: 'collapsible_2';
}
export interface Columns_2 {
columns?: Column_2[];
id?: string;
blockName?: string;
blockType: 'columns_2';
}
export interface Column_2 {
content?: (Section_3 | Collapsible_3 | Columns_3 | Tabs_3 | Accordion_3 | TextBlock | TranscriptBlock)[];
id?: string;
blockName?: string;
blockType: 'column_2';
}
export interface Tabs_2 {
tabs?: Tab_2[];
id?: string;
blockName?: string;
blockType: 'tabs_2';
}
export interface Tab_2 {
content?: (Section_3 | Collapsible_3 | Columns_3 | Tabs_3 | Accordion_3 | TextBlock | TranscriptBlock)[];
id?: string;
blockName?: string;
blockType: 'tab_2';
}
export interface Accordion_2 {
content?: Collapsible_3[];
id?: string;
blockName?: string;
blockType: 'accordion_2';
}
export interface Collapsible_1 {
content?: (Section_2 | Collapsible_2 | Columns_2 | Tabs_2 | Accordion_2 | TextBlock | TranscriptBlock)[];
id?: string;
blockName?: string;
blockType: 'collapsible_1';
}
export interface Columns_1 {
columns?: Column_1[];
id?: string;
blockName?: string;
blockType: 'columns_1';
}
export interface Column_1 {
content?: (Section_2 | Collapsible_2 | Columns_2 | Tabs_2 | Accordion_2 | TextBlock | TranscriptBlock)[];
id?: string;
blockName?: string;
blockType: 'column_1';
}
export interface Tabs_1 {
tabs?: Tab_1[];
id?: string;
blockName?: string;
blockType: 'tabs_1';
}
export interface Tab_1 {
content?: (Section_2 | Collapsible_2 | Columns_2 | Tabs_2 | Accordion_2 | TextBlock | TranscriptBlock)[];
id?: string;
blockName?: string;
blockType: 'tab_1';
}
export interface Accordion_1 {
content?: Collapsible_2[];
id?: string;
blockName?: string;
blockType: 'accordion_1';
}
export interface Tag {
id: string;
slug: string;
type:
| 'Contents'
| 'LibraryAudio'
| 'LibraryVideo'
| 'LibraryTextual'
| 'LibraryGroup'
| 'Library'
| 'Weapons'
| 'GamePlatforms'
| 'Categories';
translations?: CategoryTranslations;
}
export interface Language {
id: string;
name: string;
}
export interface Recorder {
id: string;
avatar?: string | Image;
username: string;
anonymize: boolean;
languages?: string[] | Language[];
biographies?: RecorderBiographies;
}
export interface File {
id: string;
filename: string;
type: 'LibraryScans' | 'LibrarySoundtracks' | 'ContentVideo' | 'ContentAudio';
updatedAt: string;
createdAt: string;
}
export interface Post {
id: string;
slug: string;
thumbnail?: string | Image;
authors:
| {
value: string;
relationTo: 'recorders';
}[]
| {
value: Recorder;
relationTo: 'recorders';
}[];
categories?:
| {
value: string;
relationTo: 'tags';
}[]
| {
value: Tag;
relationTo: 'tags';
}[];
translations: {
language: string | Language;
sourceLanguage: string | Language;
title: string;
summary?: string;
translators?: string[] | Recorder[];
proofreaders?: string[] | Recorder[];
content?: {
[k: string]: unknown;
}[];
id?: string;
}[];
publishedDate: string;
updatedAt: string;
createdAt: string;
_status?: 'draft' | 'published';
}
export interface User {
id: string;
email: string;
resetPasswordToken?: string;
resetPasswordExpiration?: string;
salt?: string;
hash?: string;
loginAttempts?: number;
lockUntil?: string;
password?: string;
}

View File

@ -1,4 +0,0 @@
export type AdminComponent<T extends Record<string, any>> = (props: {
data: Partial<T>;
index?: number;
}) => string;

View File

@ -1,7 +1,11 @@
import ISO6391 from "iso-639-1";
import slugify from "slugify";
export const shortenEllipsis = (text: string, length: number): string =>
text.length - 3 > length ? `${text.substring(0, length)}...` : text;
export const formatLanguageCode = (code: string): string =>
ISO6391.validate(code) ? ISO6391.getName(code) : code;
export const collectionSlug = (text: string): string =>
slugify(text, { lower: true, strict: true, trim: true });