Lots as always

This commit is contained in:
DrMint 2024-03-01 00:26:06 +01:00
parent b91159e61f
commit 18142c7f31
35 changed files with 1720 additions and 750 deletions

676
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -24,12 +24,12 @@
"dependencies": {
"@fontsource/vollkorn": "5.0.18",
"@payloadcms/bundler-webpack": "1.0.6",
"@payloadcms/db-mongodb": "1.4.0",
"@payloadcms/richtext-lexical": "0.5.2",
"@payloadcms/db-mongodb": "1.4.3",
"@payloadcms/richtext-lexical": "0.7.0",
"cross-env": "7.0.3",
"language-tags": "1.0.9",
"luxon": "3.4.4",
"payload": "2.9.0",
"payload": "2.11.2",
"sharp": "0.33.2",
"styled-components": "6.1.8"
},
@ -38,14 +38,14 @@
"@types/express": "4.17.21",
"@types/language-tags": "1.0.4",
"@types/luxon": "3.4.2",
"@types/qs": "6.9.11",
"@types/qs": "6.9.12",
"@types/react-router-dom": "5.3.3",
"@types/styled-components": "5.1.34",
"copyfiles": "2.4.1",
"nodemon": "3.0.3",
"npm-check-updates": "16.14.14",
"prettier": "3.2.4",
"ts-node": "10.9.1",
"nodemon": "3.1.0",
"npm-check-updates": "16.14.15",
"prettier": "3.2.5",
"ts-node": "10.9.2",
"ts-unused-exports": "10.0.1",
"typescript": "5.3.3"
}

View File

@ -1,5 +1,6 @@
import { Block } from "payload/types";
import { createEditor } from "../utils/editor";
import { cueBlock } from "./cueBlock";
export const lineBlock: Block = {
slug: "lineBlock",
@ -14,7 +15,7 @@ export const lineBlock: Block = {
admin: {
className: "reduced-margins",
},
editor: createEditor({ inlines: true, lists: true, links: true }),
editor: createEditor({ inlines: true, lists: true, links: true, blocks: [cueBlock] }),
},
],
};

View File

@ -28,7 +28,7 @@ export const getAllEndpoint: CollectionEndpoint = {
const eras: ChronologyEra[] = (
await payload.find({
collection: Collections.ChronologyEras,
limit: 100,
pagination: false,
})
).docs;

View File

@ -1,12 +1,13 @@
import { Where } from "payload/types";
import { sectionBlock } from "../../blocks/sectionBlock";
import { transcriptBlock } from "../../blocks/transcriptBlock";
import { CollectionGroups, Collections, FileTypes, KeysTypes } from "../../constants";
import { CollectionGroups, Collections, FileTypes } from "../../constants";
import { backPropagationField } from "../../fields/backPropagationField/backPropagationField";
import { fileField } from "../../fields/fileField/fileField";
import { imageField } from "../../fields/imageField/imageField";
import { keysField } from "../../fields/keysField/keysField";
import { rowField } from "../../fields/rowField/rowField";
import { slugField } from "../../fields/slugField/slugField";
import { tagsField } from "../../fields/tagsField/tagsField";
import { translatedFields } from "../../fields/translatedFields/translatedFields";
import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo";
import { beforeDuplicatePiping } from "../../hooks/beforeDuplicatePiping";
@ -14,14 +15,13 @@ import { beforeDuplicateUnpublish } from "../../hooks/beforeDuplicateUnpublish";
import { isDefined } from "../../utils/asserts";
import { createEditor } from "../../utils/editor";
import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig";
import { getBySlugEndpoint } from "./endpoints/getBySlugEndpoint";
import { importFromStrapi } from "./endpoints/importFromStrapi";
import { importRelationsFromStrapi } from "./endpoints/importRelationsFromStrapi";
const fields = {
slug: "slug",
thumbnail: "thumbnail",
categories: "categories",
type: "type",
translations: "translations",
pretitle: "pretitle",
title: "title",
@ -42,6 +42,7 @@ const fields = {
nextContents: "nextContents",
folders: "folders",
libraryItems: "libraryItems",
tags: "tags",
} as const satisfies Record<string, string>;
export const Contents = buildVersionedCollectionConfig({
@ -55,14 +56,7 @@ export const Contents = buildVersionedCollectionConfig({
useAsTitle: fields.slug,
description:
"All the contents (textual, audio, and video) from the Library or other online sources.",
defaultColumns: [
fields.thumbnail,
fields.slug,
fields.categories,
fields.type,
fields.translations,
fields.status,
],
defaultColumns: [fields.thumbnail, fields.slug, fields.translations, fields.status],
group: CollectionGroups.Collections,
hooks: {
beforeDuplicate: beforeDuplicatePiping([
@ -71,7 +65,7 @@ export const Contents = buildVersionedCollectionConfig({
]),
},
},
endpoints: [importFromStrapi, importRelationsFromStrapi],
endpoints: [importFromStrapi, importRelationsFromStrapi, getBySlugEndpoint],
fields: [
rowField([
slugField({ name: fields.slug }),
@ -80,23 +74,7 @@ export const Contents = buildVersionedCollectionConfig({
relationTo: Collections.ContentsThumbnails,
}),
]),
rowField([
keysField({
name: fields.categories,
relationTo: KeysTypes.Categories,
hasMany: true,
}),
keysField({
name: fields.type,
relationTo: KeysTypes.Contents,
}),
backPropagationField({
name: fields.libraryItems,
hasMany: true,
relationTo: Collections.LibraryItems,
where: ({ id }) => ({ "contents.content": { equals: id } }),
}),
]),
tagsField({ name: fields.tags }),
translatedFields({
name: fields.translations,
admin: { useAsTitle: fields.title, hasSourceLanguage: true },
@ -217,19 +195,30 @@ export const Contents = buildVersionedCollectionConfig({
rowField([
backPropagationField({
name: fields.folders,
relationTo: Collections.Folders,
hasMany: true,
relationTo: Collections.ContentsFolders,
where: ({ id }) => ({ contents: { equals: id } }),
admin: {
description: `You can set the folder(s) from the "Contents Folders" collection`,
},
where: ({ id }) => ({
and: [
{ "files.value": { equals: id } },
{ "files.relationTo": { equals: Collections.Contents } },
] as Where[],
}),
}),
backPropagationField({
name: fields.libraryItems,
hasMany: true,
relationTo: Collections.LibraryItems,
where: ({ id }) => ({ "contents.content": { equals: id } }),
}),
]),
rowField([
backPropagationField({
name: fields.previousContents,
relationTo: Collections.Contents,
hasMany: true,
where: ({ id }) => ({ [fields.nextContents]: { equals: id } }),
}),
{
name: fields.nextContents,
type: "relationship",

View File

@ -0,0 +1,50 @@
import { Collections } from "../../../constants";
import { createGetByEndpoint } from "../../../endpoints/createGetByEndpoint";
import { EndpointContent } from "../../../sdk";
import { Content } from "../../../types/collections";
import { isPayloadArrayType, isPayloadType, isValidPayloadImage } from "../../../utils/asserts";
import { convertTagsToGroups } from "../../../utils/tags";
export const getBySlugEndpoint = createGetByEndpoint(
Collections.Contents,
"slug",
({ thumbnail, slug, translations, tags }: Content): EndpointContent => ({
slug,
...(isValidPayloadImage(thumbnail) ? { thumbnail } : {}),
tagGroups: convertTagsToGroups(tags),
translations: translations.map((translation) => {
const { language, sourceLanguage, title, subtitle, pretitle, summary } = translation;
const text = handleTextContent(translation);
return {
language: isPayloadType(language) ? language.id : language,
sourceLanguage: isPayloadType(sourceLanguage) ? sourceLanguage.id : sourceLanguage,
...(pretitle ? { pretitle } : {}),
title,
...(subtitle ? { subtitle } : {}),
...(summary ? { summary } : {}),
format: { ...(text ? { text } : {}) },
};
}),
})
);
const handleTextContent = ({
textContent,
textNotes,
textProofreaders,
textTranscribers,
textTranslators,
}: Content["translations"][number]): EndpointContent["translations"][number]["format"]["text"] => {
if (!textContent) return undefined;
return {
content: textContent,
toc: [],
translators: isPayloadArrayType(textTranslators) ? textTranslators.map(({ id }) => id) : [],
transcribers: isPayloadArrayType(textTranscribers) ? textTranscribers.map(({ id }) => id) : [],
proofreaders: isPayloadArrayType(textProofreaders) ? textProofreaders.map(({ id }) => id) : [],
...(textNotes ? { notes: textNotes } : {}),
};
};

View File

@ -1,56 +0,0 @@
import { CollectionGroups, Collections } from "../../constants";
import { rowField } from "../../fields/rowField/rowField";
import { slugField } from "../../fields/slugField/slugField";
import { translatedFields } from "../../fields/translatedFields/translatedFields";
import { buildCollectionConfig } from "../../utils/collectionConfig";
import { importFromStrapi } from "./endpoints/importFromStrapi";
const fields = {
slug: "slug",
translations: "translations",
name: "name",
subfolders: "subfolders",
contents: "contents",
} as const satisfies Record<string, string>;
export const ContentsFolders = buildCollectionConfig({
slug: Collections.ContentsFolders,
labels: {
singular: "Contents Folder",
plural: "Contents Folders",
},
defaultSort: fields.slug,
admin: {
useAsTitle: fields.slug,
defaultColumns: [fields.slug, fields.translations],
disableDuplicate: true,
group: CollectionGroups.Collections,
},
endpoints: [importFromStrapi],
timestamps: false,
versions: false,
fields: [
slugField({ name: fields.slug }),
translatedFields({
name: fields.translations,
admin: {
useAsTitle: fields.name,
},
fields: [{ name: fields.name, type: "text", required: true }],
}),
rowField([
{
type: "relationship",
name: fields.subfolders,
relationTo: Collections.ContentsFolders,
hasMany: true,
},
{
type: "relationship",
name: fields.contents,
relationTo: Collections.Contents,
hasMany: true,
},
]),
],
});

View File

@ -1,125 +0,0 @@
import payload from "payload";
import QueryString from "qs";
import { Collections } from "../../../constants";
import { CollectionEndpoint } from "../../../types/payload";
import { StrapiLanguage } from "../../../types/strapi";
import { isUndefined } from "../../../utils/asserts";
import { findContent } from "../../../utils/localApi";
type StrapiContentsFolder = {
id: string;
attributes: {
slug: string;
titles?: { title: string; language: StrapiLanguage }[];
subfolders: { data: StrapiContentsFolder[] };
contents: { data: { attributes: { slug: string } }[] };
};
};
const getStrapiContentFolder = async (id: number): Promise<StrapiContentsFolder> => {
const paramsWithPagination = QueryString.stringify({
populate: [
"contents",
"subfolders",
"subfolders.contents",
"subfolders.titles",
"subfolders.titles.language",
"subfolders.subfolders",
"subfolders.subfolders.contents",
"subfolders.subfolders.titles",
"subfolders.subfolders.titles.language",
"subfolders.subfolders.subfolders",
"subfolders.subfolders.subfolders.contents",
"subfolders.subfolders.subfolders.titles",
"subfolders.subfolders.subfolders.titles.language",
"subfolders.subfolders.subfolders.subfolders",
"subfolders.subfolders.subfolders.subfolders.contents",
"subfolders.subfolders.subfolders.subfolders.titles",
"subfolders.subfolders.subfolders.subfolders.titles.language",
"subfolders.subfolders.subfolders.subfolders.subfolders",
"subfolders.subfolders.subfolders.subfolders.subfolders.contents",
"subfolders.subfolders.subfolders.subfolders.subfolders.titles",
"subfolders.subfolders.subfolders.subfolders.subfolders.titles.language",
"subfolders.subfolders.subfolders.subfolders.subfolders.subfolders",
"subfolders.subfolders.subfolders.subfolders.subfolders.subfolders.contents",
"subfolders.subfolders.subfolders.subfolders.subfolders.subfolders.titles",
"subfolders.subfolders.subfolders.subfolders.subfolders.subfolders.titles.language",
"subfolders.subfolders.subfolders.subfolders.subfolders.subfolders.subfolders",
"subfolders.subfolders.subfolders.subfolders.subfolders.subfolders.subfolders.contents",
"subfolders.subfolders.subfolders.subfolders.subfolders.subfolders.subfolders.titles",
"subfolders.subfolders.subfolders.subfolders.subfolders.subfolders.subfolders.titles.language",
],
});
const uri = `${process.env.STRAPI_URI}/api/contents-folders/${id}?${paramsWithPagination}`;
const fetchResult = await fetch(uri, {
method: "get",
headers: { authorization: `Bearer ${process.env.STRAPI_TOKEN}` },
});
const { data } = await fetchResult.json();
return data;
};
export const importFromStrapi: CollectionEndpoint = {
method: "post",
path: "/strapi",
handler: async (req, res) => {
if (!req.user) {
return res.status(403).send({
errors: [
{
message: "You are not allowed to perform this action.",
},
],
});
}
let foldersCreated = 0;
const errors: string[] = [];
const createContentFolder = async (data: StrapiContentsFolder): Promise<string> => {
const { slug, titles } = data.attributes;
const subfolders = await Promise.all(
data.attributes.subfolders.data.map(createContentFolder)
);
const contents: string[] = [];
for (const content of data.attributes.contents.data) {
try {
const result = await findContent(content.attributes.slug);
contents.push(result);
} catch (e) {
errors.push(`Couldn't add ${content.attributes.slug} to folder ${slug}`);
}
}
const result = await payload.create({
collection: Collections.ContentsFolders,
data: {
slug,
subfolders,
contents,
translations: titles?.map(({ title, language }) => {
if (isUndefined(language.data))
throw new Error("A language is required for a content folder translation");
return { language: language.data.attributes.code, name: title };
}),
},
user: req.user,
});
foldersCreated++;
return result.id;
};
const rootFolder = await getStrapiContentFolder(72);
try {
await createContentFolder(rootFolder);
} catch (e) {
res.status(500).json({ message: "Something went wrong", error: e });
}
res
.status(200)
.json({ message: `${foldersCreated} entries have been added successfully.`, errors });
},
};

View File

@ -20,11 +20,11 @@ export const getAllEndpoint: CollectionEndpoint = {
const currencies: Currency[] = (
await payload.find({
collection: Collections.Currencies,
limit: 100,
sort: "id"
sort: "id",
pagination: false,
})
).docs;
res.status(200).header("Cache-Control", "max-age=60").json(currencies);
res.status(200).json(currencies);
},
};

View File

@ -83,7 +83,7 @@ export const Folders = buildCollectionConfig({
{
type: "relationship",
name: fields.files,
relationTo: [Collections.LibraryItems, Collections.Contents],
relationTo: [Collections.LibraryItems, Collections.Contents, Collections.Pages],
hasMany: true,
},
],

View File

@ -1,8 +1,8 @@
import { Collections } from "../../../constants";
import { createGetByEndpoint } from "../../../endpoints/createGetByEndpoint";
import { EndpointFolder, EndpointFolderPreview, PayloadImage } from "../../../sdk";
import { Folder, FoldersThumbnail, Language } from "../../../types/collections";
import { isDefined, isUndefined, isValidPayloadImage } from "../../../utils/asserts";
import { EndpointFolder, EndpointFolderPreview } from "../../../sdk";
import { Folder, Language } from "../../../types/collections";
import { isDefined, isPayloadType, isValidPayloadImage } from "../../../utils/asserts";
export const getBySlugEndpoint = createGetByEndpoint(
Collections.Folders,
@ -15,7 +15,7 @@ export const getBySlugEndpoint = createGetByEndpoint(
? {
type: "single",
subfolders:
folder.sections[0]?.subfolders?.filter(isValidFolder).map(convertFolderToPreview) ??
folder.sections[0]?.subfolders?.filter(isPayloadType).map(convertFolderToPreview) ??
[],
}
: {
@ -29,6 +29,20 @@ export const getBySlugEndpoint = createGetByEndpoint(
subfolders: subfolders.map(convertFolderToPreview),
})) ?? [],
},
files:
folder.files?.flatMap<EndpointFolder["files"][number]>(({ relationTo, value }) => {
if (!isPayloadType(value)) {
return [];
}
switch (relationTo) {
case "contents":
return [{ relationTo, value }];
case "library-items":
return [{ relationTo, value }];
case "pages":
return [{ relationTo, value }];
}
}) ?? [],
};
}
);
@ -47,10 +61,10 @@ export const convertFolderToPreview = ({
translations?.map(({ language, name, description }) => ({
language: getLanguageId(language),
name,
description: JSON.stringify(description),
...(description ? { description } : {}),
})) ?? [],
darkThumbnail: getThumbnail(darkThumbnail),
lightThumbnail: getThumbnail(lightThumbnail),
darkThumbnail: isValidPayloadImage(darkThumbnail) ? darkThumbnail : undefined,
lightThumbnail: isValidPayloadImage(lightThumbnail) ? lightThumbnail : undefined,
};
};
@ -77,19 +91,7 @@ const isValidSection = (section: {
if (!section.subfolders) {
return false;
}
return section.subfolders.every(isValidFolder);
};
export const isValidFolder = (folder: string | Folder): folder is Folder =>
typeof folder !== "string";
const getThumbnail = (
thumbnail: string | FoldersThumbnail | null | undefined
): PayloadImage | undefined => {
if (isUndefined(thumbnail)) return undefined;
if (typeof thumbnail === "string") return undefined;
if (!isValidPayloadImage(thumbnail)) return undefined;
return thumbnail;
return section.subfolders.every(isPayloadType);
};
const getLanguageId = (language: string | Language) =>

View File

@ -2,7 +2,8 @@ import payload from "payload";
import { Collections } from "../../../constants";
import { EndpointFolderPreview } from "../../../sdk";
import { CollectionEndpoint } from "../../../types/payload";
import { convertFolderToPreview, isValidFolder } from "./getBySlugEndpoint";
import { isPayloadType } from "../../../utils/asserts";
import { convertFolderToPreview } from "./getBySlugEndpoint";
export const getRootFoldersEndpoint: CollectionEndpoint = {
@ -39,7 +40,7 @@ export const getRootFoldersEndpoint: CollectionEndpoint = {
return;
}
const result = folders.filter(isValidFolder).map<EndpointFolderPreview>(convertFolderToPreview);
const result = folders.filter(isPayloadType).map<EndpointFolderPreview>(convertFolderToPreview);
res.status(200).json(result);
},

View File

@ -16,18 +16,7 @@ export const FoldersThumbnails = buildImageCollectionConfig({
},
admin: { defaultColumns: [fields.filename, fields.updatedAt] },
upload: {
imageSizes: [
{
name: "medium",
height: 400,
width: 200,
fit: "contain",
formatOptions: {
format: "webp",
options: { effort: 6, quality: 80, alphaQuality: 80 },
},
},
],
imageSizes: [],
},
fields: [],
});

View File

@ -0,0 +1,33 @@
import { Collections } from "../../constants";
import { buildImageCollectionConfig } from "../../utils/imageCollectionConfig";
const fields = {
filename: "filename",
mimeType: "mimeType",
filesize: "filesize",
posts: "posts",
updatedAt: "updatedAt",
} as const satisfies Record<string, string>;
export const Images = buildImageCollectionConfig({
slug: Collections.Images,
labels: {
singular: "Image",
plural: "Images",
},
admin: { defaultColumns: [fields.filename, fields.posts, fields.updatedAt] },
upload: {
imageSizes: [
{
name: "og",
height: 750,
width: 1125,
formatOptions: {
format: "jpg",
options: { progressive: true, mozjpeg: true, compressionLevel: 9, quality: 60 },
},
},
],
},
fields: [],
});

View File

@ -8,6 +8,7 @@ import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo";
import { Key } from "../../types/collections";
import { isDefined, isUndefined } from "../../utils/asserts";
import { buildCollectionConfig } from "../../utils/collectionConfig";
import { getAllEndpoint } from "./endpoints/getAllEndpoint";
import { importFromStrapi } from "./endpoints/importFromStrapi";
const fields = {
@ -74,7 +75,7 @@ export const Keys = buildCollectionConfig({
},
],
},
endpoints: [importFromStrapi],
endpoints: [importFromStrapi, getAllEndpoint],
timestamps: false,
versions: false,
fields: [

View File

@ -0,0 +1,42 @@
import payload from "payload";
import { Collections } from "../../../constants";
import { EndpointKey } from "../../../sdk";
import { Key } from "../../../types/collections";
import { CollectionEndpoint } from "../../../types/payload";
import { isPayloadType } from "../../../utils/asserts";
export const getAllEndpoint: CollectionEndpoint = {
method: "get",
path: "/all",
handler: async (req, res) => {
if (!req.user) {
return res.status(403).send({
errors: [
{
message: "You are not allowed to perform this action.",
},
],
});
}
const keys: Key[] = (
await payload.find({
collection: Collections.Keys,
sort: "id",
pagination: false,
})
).docs;
const result: EndpointKey[] = keys.map(({ translations, ...others }) => ({
...others,
translations:
translations?.map(({ language, name, short }) => ({
language: isPayloadType(language) ? language.id : language,
name,
short: short ?? name,
})) ?? [],
}));
res.status(200).json(result);
},
};

View File

@ -20,8 +20,8 @@ export const getAllEndpoint: CollectionEndpoint = {
const languages: Language[] = (
await payload.find({
collection: Collections.Languages,
limit: 100,
sort: "name"
sort: "name",
pagination: false,
})
).docs;

View File

@ -0,0 +1,215 @@
import { Where } from "payload/types";
import { sectionBlock } from "../../blocks/sectionBlock";
import { QuickFilters, publishStatusFilters } from "../../components/QuickFilters";
import { CollectionGroups, Collections, PageType } from "../../constants";
import { backPropagationField } from "../../fields/backPropagationField/backPropagationField";
import { imageField } from "../../fields/imageField/imageField";
import { rowField } from "../../fields/rowField/rowField";
import { slugField } from "../../fields/slugField/slugField";
import { tagsField } from "../../fields/tagsField/tagsField";
import { translatedFields } from "../../fields/translatedFields/translatedFields";
import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo";
import { beforeDuplicatePiping } from "../../hooks/beforeDuplicatePiping";
import { beforeDuplicateUnpublish } from "../../hooks/beforeDuplicateUnpublish";
import { isDefined, isUndefined } from "../../utils/asserts";
import { createEditor } from "../../utils/editor";
import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig";
import { getBySlugEndpoint } from "./endpoints/getBySlugEndpoint";
const fields = {
slug: "slug",
type: "type",
authors: "authors",
thumbnail: "thumbnail",
translations: "translations",
tags: "tags",
sourceLanguage: "sourceLanguage",
pretitle: "pretitle",
title: "title",
subtitle: "subtitle",
summary: "summary",
content: "content",
transcribers: "transcribers",
translators: "translators",
proofreaders: "proofreaders",
collectibles: "collectibles",
folders: "folders",
} as const satisfies Record<string, string>;
const pageTypesWithAuthor = [PageType.Article];
const pageTypesWithCollectibles = [PageType.Content];
const pageTypesWithTranscribers = [PageType.Content];
export const Pages = buildVersionedCollectionConfig({
slug: Collections.Pages,
labels: {
singular: "Page",
plural: "Pages",
},
defaultSort: fields.slug,
admin: {
useAsTitle: fields.slug,
defaultColumns: [fields.thumbnail, fields.slug],
group: CollectionGroups.Collections,
components: {
BeforeListTable: [
() =>
QuickFilters({
slug: Collections.Posts,
filterGroups: [publishStatusFilters],
}),
],
},
hooks: {
beforeDuplicate: beforeDuplicatePiping([
beforeDuplicateUnpublish,
beforeDuplicateAddCopyTo(fields.slug),
]),
},
},
endpoints: [getBySlugEndpoint],
fields: [
{
name: fields.type,
type: "radio",
required: true,
defaultValue: PageType.Generic,
options: Object.entries(PageType).map(([value, label]) => ({ label, value })),
},
rowField([
slugField({ name: fields.slug }),
imageField({
name: fields.thumbnail,
relationTo: Collections.Images,
}),
]),
rowField([
tagsField({ name: fields.tags }),
{
name: fields.authors,
type: "relationship",
admin: {
condition: (_, siblingData) => pageTypesWithAuthor.includes(siblingData[fields.type]),
},
relationTo: Collections.Recorders,
required: true,
minRows: 1,
hasMany: true,
},
]),
translatedFields({
name: fields.translations,
admin: { useAsTitle: fields.title, hasSourceLanguage: true },
required: true,
minRows: 1,
fields: [
rowField([
{ name: fields.pretitle, type: "text" },
{ name: fields.title, type: "text", required: true },
{ name: fields.subtitle, type: "text" },
]),
{
name: fields.summary,
type: "richText",
editor: createEditor({ inlines: true, lists: true, links: true }),
},
{
name: fields.content,
type: "richText",
required: true,
editor: createEditor({
images: true,
inlines: true,
alignment: true,
blocks: [sectionBlock],
links: true,
lists: true,
}),
},
rowField([
{
name: fields.transcribers,
type: "relationship",
relationTo: Collections.Recorders,
hasMany: true,
admin: {
condition: (data, siblingData) => {
if (!pageTypesWithTranscribers.includes(data[fields.type])) {
return false;
}
if (isUndefined(siblingData.language) || isUndefined(siblingData.sourceLanguage)) {
return false;
}
return siblingData.language === siblingData.sourceLanguage;
},
},
},
{
name: fields.translators,
type: "relationship",
relationTo: Collections.Recorders,
hasMany: true,
hooks: {
beforeChange: [
({ siblingData }) => {
if (siblingData.language === siblingData.sourceLanguage) {
delete siblingData.translators;
}
},
],
},
admin: {
condition: (_, siblingData) => {
if (isUndefined(siblingData.language) || isUndefined(siblingData.sourceLanguage)) {
return false;
}
return siblingData.language !== siblingData.sourceLanguage;
},
},
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: Collections.Recorders,
hasMany: true,
},
]),
],
}),
rowField([
backPropagationField({
name: fields.folders,
relationTo: Collections.Folders,
hasMany: true,
where: ({ id }) => ({
and: [
{ "files.value": { equals: id } },
{ "files.relationTo": { equals: Collections.Pages } },
] as Where[],
}),
}),
backPropagationField({
name: fields.collectibles,
hasMany: true,
relationTo: Collections.LibraryItems,
admin: {
condition: (_, siblingData) =>
pageTypesWithCollectibles.includes(siblingData[fields.type]),
},
where: ({ id }) => ({ "contents.content": { equals: id } }),
}),
]),
],
});

View File

@ -0,0 +1,134 @@
import {
Collections,
PageType,
RichTextContent,
isBlockNodeSectionBlock,
isNodeBlockNode,
} from "../../../constants";
import { createGetByEndpoint } from "../../../endpoints/createGetByEndpoint";
import { EndpointPage, ParentPage, TableOfContentEntry } from "../../../sdk";
import { Page } from "../../../types/collections";
import { isPayloadArrayType, isPayloadType, isValidPayloadImage } from "../../../utils/asserts";
import { convertTagsToGroups } from "../../../utils/tags";
export const getBySlugEndpoint = createGetByEndpoint(
Collections.Pages,
"slug",
({
authors,
slug,
translations,
tags,
thumbnail,
_status,
collectibles,
folders,
type,
}: Page): EndpointPage => ({
slug,
type: type as PageType,
...(isValidPayloadImage(thumbnail) ? { thumbnail } : {}),
tagGroups: convertTagsToGroups(tags),
translations: translations.map(
({
content,
language,
sourceLanguage,
title,
pretitle,
subtitle,
proofreaders,
summary,
transcribers,
translators,
}) => ({
language: isPayloadType(language) ? language.id : language,
sourceLanguage: isPayloadType(sourceLanguage) ? sourceLanguage.id : sourceLanguage,
...(pretitle ? { pretitle } : {}),
title,
...(subtitle ? { subtitle } : {}),
...(summary ? { summary } : {}),
content: handleContent(content),
toc: handleToc(content),
translators: isPayloadArrayType(translators) ? translators.map(({ id }) => id) : [],
transcribers: isPayloadArrayType(transcribers) ? transcribers.map(({ id }) => id) : [],
proofreaders: isPayloadArrayType(proofreaders) ? proofreaders.map(({ id }) => id) : [],
})
),
authors: isPayloadArrayType(authors) ? authors.map(({ id }) => id) : [],
status: _status === "published" ? "published" : "draft",
parentPages: handleParentPages({ collectibles, folders }),
})
);
const handleContent = (
{ root: { children, ...others } }: RichTextContent,
parentPrefix = ""
): RichTextContent => {
let sectionCount = 0;
return {
root: {
...others,
children: children.map((node) => {
if (isNodeBlockNode(node) && isBlockNodeSectionBlock(node)) {
sectionCount++;
const anchorHash = `${parentPrefix}${sectionCount}.`;
return {
...node,
fields: {
...node.fields,
anchorHash,
lines: handleContent(node.fields.lines, anchorHash),
},
};
}
return node;
}),
},
};
};
const handleToc = (content: RichTextContent, parentPrefix = ""): TableOfContentEntry[] =>
content.root.children
.filter(isNodeBlockNode)
.filter(isBlockNodeSectionBlock)
.map(({ fields }, index) => ({
prefix: `${parentPrefix}${index + 1}.`,
title: fields.blockName,
children: handleToc(fields.lines, `${index + 1}.`),
}));
const handleParentPages = ({
collectibles,
folders,
}: Pick<Page, "collectibles" | "folders">): ParentPage[] => {
const result: ParentPage[] = [];
if (collectibles && isPayloadArrayType(collectibles)) {
collectibles.forEach(({ slug, title }) => {
result.push({
collection: Collections.LibraryItems,
slug,
translations: [{ language: "en", name: title }],
tag: "collectible",
});
});
}
if (folders && isPayloadArrayType(folders)) {
folders.forEach(({ slug, translations }) => {
result.push({
collection: Collections.Folders,
slug,
translations:
translations?.map(({ language, name }) => ({
language: isPayloadType(language) ? language.id : language,
name,
})) ?? [],
tag: "folders",
});
});
}
return result;
};

View File

@ -8,6 +8,7 @@ import { rowField } from "../../fields/rowField/rowField";
import { translatedFields } from "../../fields/translatedFields/translatedFields";
import { buildCollectionConfig } from "../../utils/collectionConfig";
import { createEditor } from "../../utils/editor";
import { getAllEndpoint } from "./endpoints/getAllEndpoint";
import { importFromStrapi } from "./endpoints/importFromStrapi";
import { beforeLoginMustHaveAtLeastOneRole } from "./hooks/beforeLoginMustHaveAtLeastOneRole";
@ -75,7 +76,7 @@ export const Recorders = buildCollectionConfig({
hooks: {
beforeLogin: [beforeLoginMustHaveAtLeastOneRole],
},
endpoints: [importFromStrapi],
endpoints: [importFromStrapi, getAllEndpoint],
timestamps: false,
fields: [
rowField([

View File

@ -0,0 +1,45 @@
import payload from "payload";
import { Collections } from "../../../constants";
import { EndpointRecorder } from "../../../sdk";
import { CollectionEndpoint } from "../../../types/payload";
import { isPayloadArrayType, isPayloadType, isValidPayloadImage } from "../../../utils/asserts";
export const getAllEndpoint: CollectionEndpoint = {
method: "get",
path: "/all",
handler: async (req, res) => {
if (!req.user) {
return res.status(403).send({
errors: [
{
message: "You are not allowed to perform this action.",
},
],
});
}
const recorders = (
await payload.find({
collection: Collections.Recorders,
sort: "id",
pagination: false,
})
).docs;
const result: EndpointRecorder[] = recorders.map(
({ anonymize, id, username, avatar, biographies, languages }) => ({
id,
username: anonymize ? `Recorder#${id.substring(0, 5)}` : username,
...(isValidPayloadImage(avatar) ? { avatar } : {}),
languages: isPayloadArrayType(languages) ? languages.map(({ id }) => id) : [],
biographies:
biographies?.map(({ biography, language }) => ({
language: isPayloadType(language) ? language.id : language,
biography,
})) ?? [],
})
);
res.status(200).json(result);
},
};

View File

@ -0,0 +1,56 @@
import payload from "payload";
import { CollectionBeforeChangeHook, CollectionConfig } from "payload/types";
import { CollectionGroups, Collections } from "../../constants";
import { slugField } from "../../fields/slugField/slugField";
import { translatedFields } from "../../fields/translatedFields/translatedFields";
import { buildCollectionConfig } from "../../utils/collectionConfig";
import { getAllEndpoint } from "./endpoints/getAllEndpoint";
const beforeChangeUpdateName: CollectionBeforeChangeHook = async ({ data }) => {
let name = data.slug;
const parentId = data[fields.group];
if (parentId) {
const parent = await payload.findByID({
collection: Collections.TagsGroups,
id: data[fields.group],
});
name = `${parent.slug} / ${data.slug}`;
}
return {
...data,
[fields.name]: name,
};
};
const fields = {
slug: "slug",
name: "name",
translations: "translations",
translationsName: "name",
group: "group",
};
export const Tags: CollectionConfig = buildCollectionConfig({
slug: Collections.Tags,
labels: { singular: "Tag", plural: "Tags" },
admin: {
group: CollectionGroups.Meta,
useAsTitle: fields.name,
defaultColumns: [fields.slug, fields.group, fields.translations],
},
endpoints: [getAllEndpoint],
hooks: { beforeChange: [beforeChangeUpdateName] },
fields: [
{ name: fields.name, type: "text", admin: { readOnly: true, hidden: true } },
slugField({ name: fields.slug }),
translatedFields({
name: fields.translations,
admin: { useAsTitle: fields.translationsName },
fields: [{ name: fields.translationsName, type: "text", required: true }],
}),
{ name: fields.group, type: "relationship", required: true, relationTo: Collections.TagsGroups },
],
});

View File

@ -0,0 +1,41 @@
import payload from "payload";
import { Collections } from "../../../constants";
import { EndpointTag } from "../../../sdk";
import { CollectionEndpoint } from "../../../types/payload";
import { isPayloadType } from "../../../utils/asserts";
export const getAllEndpoint: CollectionEndpoint = {
method: "get",
path: "/all",
handler: async (req, res) => {
if (!req.user) {
return res.status(403).send({
errors: [
{
message: "You are not allowed to perform this action.",
},
],
});
}
const tags = (
await payload.find({
collection: Collections.Tags,
sort: "id",
pagination: false,
})
).docs;
const result = tags.map<EndpointTag>(({ slug, translations, group }) => ({
slug,
translations:
translations?.map(({ language, name }) => ({
language: isPayloadType(language) ? language.id : language,
name,
})) ?? [],
group: isPayloadType(group) ? group.slug : group,
}));
res.status(200).json(result);
},
};

View File

@ -0,0 +1,34 @@
import { CollectionConfig } from "payload/types";
import { CollectionGroups, Collections } from "../../constants";
import { iconField } from "../../fields/iconField/iconField";
import { slugField } from "../../fields/slugField/slugField";
import { translatedFields } from "../../fields/translatedFields/translatedFields";
import { buildCollectionConfig } from "../../utils/collectionConfig";
import { getAllEndpoint } from "./endpoints/getAllEndpoint";
const fields = {
slug: "slug",
translations: "translations",
translationsName: "name",
icon: "icon",
};
export const TagsGroups: CollectionConfig = buildCollectionConfig({
slug: Collections.TagsGroups,
labels: { singular: "Tags Group", plural: "Tags Groups" },
admin: {
group: CollectionGroups.Meta,
useAsTitle: fields.slug,
defaultColumns: [fields.slug, fields.translations],
},
endpoints: [getAllEndpoint],
fields: [
slugField({ name: fields.slug }),
iconField({ name: fields.icon }),
translatedFields({
name: fields.translations,
admin: { useAsTitle: fields.translationsName },
fields: [{ name: fields.translationsName, type: "text", required: true }],
}),
],
});

View File

@ -0,0 +1,42 @@
import payload from "payload";
import { Collections } from "../../../constants";
import { EndpointTagsGroup } from "../../../sdk";
import { CollectionEndpoint } from "../../../types/payload";
import { isPayloadType } from "../../../utils/asserts";
export const getAllEndpoint: CollectionEndpoint = {
method: "get",
path: "/all",
handler: async (req, res) => {
if (!req.user) {
return res.status(403).send({
errors: [
{
message: "You are not allowed to perform this action.",
},
],
});
}
const tags = (
await payload.find({
collection: Collections.TagsGroups,
sort: "id",
pagination: false,
})
).docs;
const result = tags.map<EndpointTagsGroup>(({ slug, translations, icon }) => ({
slug,
...(icon ? { icon } : {}),
translations:
translations?.map(({ language, name }) => ({
language: isPayloadType(language) ? language.id : language,
name,
})) ?? [],
tags: [] // TODO: Add tags,
}));
res.status(200).json(result);
},
};

View File

@ -1,7 +1,10 @@
import { CueBlock, LineBlock, SectionBlock, TranscriptBlock } from "./types/collections";
// END MOCKING SECTION
export enum Collections {
ChronologyEras = "chronology-eras",
ChronologyItems = "chronology-items",
ContentsFolders = "contents-folders",
Contents = "contents",
ContentsThumbnails = "contents-thumbnails",
Currencies = "currencies",
@ -12,8 +15,10 @@ export enum Collections {
LibraryItemsThumbnails = "library-items-thumbnails",
LibraryItemsScans = "library-items-scans",
LibraryItemsGallery = "library-items-gallery",
Notes = "Notes",
Notes = "notes",
Posts = "posts",
Pages = "pages",
PagesThumbnails = "pages-thumbnails",
PostsThumbnails = "posts-thumbnails",
Recorders = "recorders",
RecordersThumbnails = "recorders-thumbnails",
@ -23,7 +28,10 @@ export enum Collections {
WeaponsGroups = "weapons-groups",
WeaponsThumbnails = "weapons-thumbnails",
Folders = "folders",
FoldersThumbnails = "FoldersThumbnails",
FoldersThumbnails = "folders-thumbnails",
Tags = "tags",
TagsGroups = "tags-groups",
Images = "images",
}
export enum CollectionGroups {
@ -95,3 +103,153 @@ export enum VideoSources {
NicoNico = "NicoNico",
Tumblr = "Tumblr",
}
export enum PageType {
Content = "Content",
Article = "Article",
Generic = "Generic",
}
/* RICH TEXT */
export type RichTextContent = {
root: {
children: RichTextNode[];
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
type: string;
version: number;
};
[k: string]: unknown;
};
export type RichTextNode = {
type: string;
version: number;
[k: string]: unknown;
};
export interface RichTextNodeWithChildren extends RichTextNode {
children: RichTextNode[];
}
export interface RichTextParagraphNode extends RichTextNodeWithChildren {
type: "paragraph";
}
export interface RichTextListNode extends RichTextNode {
type: "list";
children: RichTextNodeWithChildren[];
listType: string;
}
export interface RichTextListNumberNode extends RichTextListNode {
listType: "number";
}
export interface RichTextListBulletNode extends RichTextListNode {
listType: "bullet";
}
export interface RichTextListCheckNode extends RichTextListNode {
listType: "check";
}
export interface RichTextTextNode extends RichTextNode {
type: "text";
format: number;
text: string;
}
export interface RichTextLinkNode extends RichTextNodeWithChildren {
type: "link";
fields: {
linkType: "internal" | "custom";
};
}
export interface RichTextLinkInternalNode extends RichTextLinkNode {
fields: {
linkType: "internal";
newTab: boolean;
doc: {
relationTo: string;
value: any;
};
};
}
export interface RichTextLinkCustomNode extends RichTextLinkNode {
fields: {
linkType: "custom";
newTab: boolean;
url: string;
};
}
export interface RichTextBlockNode extends RichTextNode {
type: "block";
fields: {
blockType: string;
};
}
export interface RichTextSectionBlock extends RichTextBlockNode {
fields: SectionBlock & { anchorHash: string };
}
export interface RichTextTranscriptBlock extends RichTextBlockNode {
fields: TranscriptBlock;
}
export const isNodeParagraphNode = (node: RichTextNode): node is RichTextParagraphNode =>
node.type === "paragraph";
export const isNodeListNode = (node: RichTextNode): node is RichTextListNode =>
node.type === "list";
export const isListNodeNumberListNode = (node: RichTextListNode): node is RichTextListNumberNode =>
node.listType === "number";
export const isListNodeBulletListNode = (node: RichTextListNode): node is RichTextListBulletNode =>
node.listType === "bullet";
export const isListNodeCheckListNode = (node: RichTextListNode): node is RichTextListCheckNode =>
node.listType === "check";
export const isNodeTextNode = (node: RichTextNode): node is RichTextTextNode =>
node.type === "text";
export const isNodeLinkNode = (node: RichTextNode): node is RichTextLinkNode =>
node.type === "link";
export const isLinkNodeInternalLinkNode = (
node: RichTextLinkNode
): node is RichTextLinkInternalNode => node.fields.linkType === "internal";
export const isLinkNodeCustomLinkNode = (node: RichTextLinkNode): node is RichTextLinkCustomNode =>
node.fields.linkType === "custom";
export const isNodeBlockNode = (node: RichTextNode): node is RichTextBlockNode =>
node.type === "block";
export const isBlockNodeSectionBlock = (node: RichTextBlockNode): node is RichTextSectionBlock =>
node.fields.blockType === "sectionBlock";
export const isBlockNodeTranscriptBlock = (
node: RichTextBlockNode
): node is RichTextTranscriptBlock => node.fields.blockType === "transcriptBlock";
/* BLOCKS */
export interface GenericBlock {
content: unknown;
blockType: string;
}
export const isBlockCueBlock = (block: GenericBlock): block is CueBlock =>
block.blockType === "cueBlock";
export const isBlockLineBlock = (block: GenericBlock): block is LineBlock =>
block.blockType === "lineBlock";

View File

@ -5,4 +5,8 @@ type Props = Omit<TextField, "type" | "hasMany" | "maxRows" | "minRows">;
export const iconField = (props: Props): TextField => ({
...props,
type: "text",
admin: {
description:
"Select an icon from here: https://icones.js.org/collection/material-symbols. Only outline and regular variants are usable on the website.",
},
});

View File

@ -1,10 +1,10 @@
import { FieldBase, RelationshipField } from "payload/dist/fields/config/types";
import { FieldBase, SingleRelationshipField } from "payload/dist/fields/config/types";
import { Collections, KeysTypes } from "../../constants";
type KeysField = FieldBase & {
relationTo: KeysTypes;
hasMany?: boolean;
admin?: RelationshipField["admin"];
admin?: SingleRelationshipField["admin"];
};
export const keysField = ({
@ -12,7 +12,7 @@ export const keysField = ({
hasMany = false,
admin,
...props
}: KeysField): RelationshipField => ({
}: KeysField): SingleRelationshipField => ({
...props,
admin: {
allowCreate: false,

View File

@ -0,0 +1,17 @@
import { FieldBase, SingleRelationshipField } from "payload/dist/fields/config/types";
import { Collections } from "../../constants";
type KeysField = FieldBase & {
admin?: SingleRelationshipField["admin"];
};
export const tagsField = ({ admin, ...props }: KeysField): SingleRelationshipField => ({
...props,
admin: {
allowCreate: false,
...admin,
},
type: "relationship",
hasMany: true,
relationTo: Collections.Tags,
});

View File

@ -5,12 +5,12 @@ import { buildConfig } from "payload/config";
import { ChronologyEras } from "./collections/ChronologyEras/ChronologyEras";
import { ChronologyItems } from "./collections/ChronologyItems/ChronologyItems";
import { Contents } from "./collections/Contents/Contents";
import { ContentsFolders } from "./collections/ContentsFolders/ContentsFolders";
import { ContentsThumbnails } from "./collections/ContentsThumbnails/ContentsThumbnails";
import { Currencies } from "./collections/Currencies/Currencies";
import { Files } from "./collections/Files/Files";
import { Folders } from "./collections/Folders/Folders";
import { FoldersThumbnails } from "./collections/FoldersThumbnails/FoldersThumbnails";
import { Images } from "./collections/Images/Images";
import { Keys } from "./collections/Keys/Keys";
import { Languages } from "./collections/Languages/Languages";
import { LibraryItems } from "./collections/LibraryItems/LibraryItems";
@ -18,10 +18,13 @@ import { LibraryItemsGallery } from "./collections/LibraryItemsGallery/LibraryIt
import { LibraryItemsScans } from "./collections/LibraryItemsScans/LibraryItemsScans";
import { LibraryItemsThumbnails } from "./collections/LibraryItemsThumbnails/LibraryItemsThumbnails";
import { Notes } from "./collections/Notes/Notes";
import { Pages } from "./collections/Pages/Pages";
import { Posts } from "./collections/Posts/Posts";
import { PostsThumbnails } from "./collections/PostsThumbnails/PostsThumbnails";
import { Recorders } from "./collections/Recorders/Recorders";
import { RecordersThumbnails } from "./collections/RecordersThumbnails/RecordersThumbnails";
import { Tags } from "./collections/Tags/Tags";
import { TagsGroups } from "./collections/TagsGroups/TagsGroups";
import { Videos } from "./collections/Videos/Videos";
import { VideosChannels } from "./collections/VideosChannels/VideosChannels";
import { Weapons } from "./collections/Weapons/Weapons";
@ -51,8 +54,8 @@ export default buildConfig({
FoldersThumbnails,
LibraryItems,
Contents,
ContentsFolders,
Posts,
Pages,
ChronologyItems,
ChronologyEras,
Weapons,
@ -72,6 +75,9 @@ export default buildConfig({
Currencies,
Recorders,
Keys,
Tags,
TagsGroups,
Images
],
db: mongooseAdapter({
url: process.env.MONGODB_URI ?? "mongodb://mongo:27017/payload",

View File

@ -1,5 +1,5 @@
import { Collections } from "./constants";
import { Currency, Language } from "./types/collections";
import { Collections, PageType, RichTextContent } from "./constants";
import { Content, Currency, Key, Language, LibraryItem, Page } from "./types/collections";
class NodeCache {
constructor(_params: any) {}
@ -34,8 +34,8 @@ const refreshToken = async () => {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: process.env.PAYLOAD_USER,
password: process.env.PAYLOAD_PASSWORD,
email: import.meta.env.PAYLOAD_USER,
password: import.meta.env.PAYLOAD_PASSWORD,
}),
});
logResponse(loginResult);
@ -74,7 +74,7 @@ const injectAuth = async (init?: RequestInit): Promise<RequestInit> => ({
const logResponse = (res: Response) => console.log(res.status, res.statusText, res.url);
const payloadApiUrl = (collection: Collections, endpoint?: string): string =>
`${process.env.PAYLOAD_API_URL}/${collection}${endpoint === undefined ? "" : `/${endpoint}`}`;
`${import.meta.env.PAYLOAD_API_URL}/${collection}${endpoint === undefined ? "" : `/${endpoint}`}`;
const request = async (url: string, init?: RequestInit): Promise<Response> => {
const result = await fetch(url, await injectAuth(init));
@ -159,14 +159,7 @@ export type EndpointEra = {
}[];
};
export type EndpointFolder = {
slug: string;
icon?: string;
translations: {
language: string;
name: string;
description?: string;
}[];
export type EndpointFolder = EndpointFolderPreview & {
sections:
| { type: "single"; subfolders: EndpointFolderPreview[] }
| {
@ -176,8 +169,20 @@ export type EndpointFolder = {
subfolders: EndpointFolderPreview[];
}[];
};
lightThumbnail?: PayloadImage;
darkThumbnail?: PayloadImage;
files: (
| {
relationTo: "library-items";
value: LibraryItem;
}
| {
relationTo: "contents";
value: Content;
}
| {
relationTo: "pages";
value: Page;
}
)[];
};
export type EndpointFolderPreview = {
@ -186,12 +191,115 @@ export type EndpointFolderPreview = {
translations: {
language: string;
name: string;
description?: string;
description?: RichTextContent;
}[];
lightThumbnail?: PayloadImage;
darkThumbnail?: PayloadImage;
};
export type EndpointContent = {
slug: string;
thumbnail?: PayloadImage;
tagGroups: TagGroup[];
translations: {
language: string;
sourceLanguage: string;
pretitle?: string;
title: string;
subtitle?: string;
summary?: RichTextContent;
format: {
text?: {
content: RichTextContent;
toc: TableOfContentEntry[];
transcribers: string[];
translators: string[];
proofreaders: string[];
notes?: RichTextContent;
};
};
}[];
};
export type EndpointRecorder = {
id: string;
username: string;
avatar?: PayloadImage;
languages: string[];
biographies: {
language: string;
biography: RichTextContent;
}[];
};
export type EndpointKey = {
id: string;
name: string;
type: Key["type"];
translations: {
language: string;
name: string;
short: string;
}[];
};
export type EndpointTag = {
slug: string;
translations: {
language: string;
name: string;
}[];
group: string;
};
export type EndpointTagsGroup = {
slug: string;
icon?: string;
translations: {
language: string;
name: string;
}[];
tags: EndpointTag[];
};
export type EndpointPage = {
slug: string;
type: PageType;
thumbnail?: PayloadImage;
authors: string[];
tagGroups: TagGroup[];
translations: {
language: string;
sourceLanguage: string;
pretitle?: string;
title: string;
subtitle?: string;
summary?: RichTextContent;
content: RichTextContent;
transcribers: string[];
translators: string[];
proofreaders: string[];
toc: TableOfContentEntry[];
}[];
status: "draft" | "published";
parentPages: ParentPage[];
};
export type ParentPage = {
slug: string;
collection: Collections;
translations: { language: string; name: string }[];
tag: string;
};
export type TagGroup = { slug: string; icon: string; values: string[] };
export type TableOfContentEntry = {
prefix: string;
title: string;
children: TableOfContentEntry[];
};
export type PayloadImage = {
url: string;
width: number;
@ -213,4 +321,16 @@ export const payload = {
await (await request(payloadApiUrl(Collections.Languages, `all`))).json(),
getCurrencies: async (): Promise<Currency[]> =>
await (await request(payloadApiUrl(Collections.Currencies, `all`))).json(),
getContent: async (slug: string): Promise<EndpointContent> =>
await (await request(payloadApiUrl(Collections.Contents, `slug/${slug}`))).json(),
getKeys: async (): Promise<EndpointKey[]> =>
await (await request(payloadApiUrl(Collections.Keys, `all`))).json(),
getRecorders: async (): Promise<EndpointRecorder[]> =>
await (await request(payloadApiUrl(Collections.Recorders, `all`))).json(),
getTags: async (): Promise<EndpointTag[]> =>
await (await request(payloadApiUrl(Collections.Tags, `all`))).json(),
getTagsGroups: async (): Promise<EndpointTagsGroup[]> =>
await (await request(payloadApiUrl(Collections.TagsGroups, `all`))).json(),
getPage: async (slug: string): Promise<EndpointPage> =>
await (await request(payloadApiUrl(Collections.Pages, `slug/${slug}`))).json(),
};

View File

@ -59,19 +59,32 @@ const start = async () => {
app.use("/public", express.static(path.join(__dirname, "../public")));
app.get("/api/sdk", (_, res) => {
const collections = readFileSync(path.join(__dirname, "types/collections.ts"), "utf-8");
const removeMockingSection = (text: string): string => {
const lines = text.split("\n");
const endMockingLine = lines.findIndex((line) => line === "// END MOCKING SECTION") ?? 0;
return lines.slice(endMockingLine + 1).join("\n");
};
const constantsHeader = "/////////////// CONSTANTS ///////////////\n";
const constants = readFileSync(path.join(__dirname, "constants.ts"), "utf-8");
const removeDeclare = (text: string): string => {
const lines = text.split("\n");
const startDeclareLine =
lines.findIndex((line) => line === "declare module 'payload' {") ?? 0;
return lines.slice(0, startDeclareLine).join("\n");
};
const sdkHeader = "////////////////// SDK //////////////////\n";
const sdkLines = readFileSync(path.join(__dirname, "sdk.ts"), "utf-8").split("\n");
const endMockingLine = sdkLines.findIndex((line) => line === "// END MOCKING SECTION") ?? 0;
const sdk =
`import NodeCache from "node-cache";\n\n` + sdkLines.slice(endMockingLine + 1).join("\n");
const result = [];
result.push(removeDeclare(readFileSync(path.join(__dirname, "types/collections.ts"), "utf-8")));
result.push("/////////////// CONSTANTS ///////////////");
result.push(removeMockingSection(readFileSync(path.join(__dirname, "constants.ts"), "utf-8")));
result.push("////////////////// SDK //////////////////");
result.push(`import NodeCache from "node-cache";`);
result.push(removeMockingSection(readFileSync(path.join(__dirname, "sdk.ts"), "utf-8")));
res.type("text/plain");
res.send([collections, constantsHeader, constants, sdkHeader, sdk].join("\n\n"));
res.send(result.join("\n\n"));
});
app.get("/robots.txt", (_, res) => {

View File

@ -6,6 +6,10 @@
* and re-run `payload generate:types` to regenerate this file.
*/
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "RecorderBiographies".
*/
export type RecorderBiographies =
| {
language: string | Language;
@ -27,6 +31,10 @@ export type RecorderBiographies =
id?: string | null;
}[]
| null;
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "CategoryTranslations".
*/
export type CategoryTranslations =
| {
language: string | Language;
@ -39,11 +47,11 @@ export type CategoryTranslations =
export interface Config {
collections: {
folders: Folder;
FoldersThumbnails: FoldersThumbnail;
'folders-thumbnails': FoldersThumbnail;
'library-items': LibraryItem;
contents: Content;
'contents-folders': ContentsFolder;
posts: Post;
pages: Page;
'chronology-items': ChronologyItem;
'chronology-eras': ChronologyEra;
weapons: Weapon;
@ -56,18 +64,25 @@ export interface Config {
'recorders-thumbnails': RecordersThumbnail;
'posts-thumbnails': PostThumbnail;
files: File;
Notes: Note;
notes: Note;
videos: Video;
'videos-channels': VideosChannel;
languages: Language;
currencies: Currency;
recorders: Recorder;
keys: Key;
tags: Tag;
'tags-groups': TagsGroup;
images: Image;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
globals: {};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "folders".
*/
export interface Folder {
id: string;
slug: string;
@ -119,11 +134,19 @@ export interface Folder {
relationTo: 'contents';
value: string | Content;
}
| {
relationTo: 'pages';
value: string | Page;
}
)[]
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "folders-thumbnails".
*/
export interface FoldersThumbnail {
id: string;
updatedAt: string;
@ -143,20 +166,20 @@ export interface FoldersThumbnail {
filesize?: number | null;
filename?: string | null;
};
medium?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "languages".
*/
export interface Language {
id: string;
name: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "library-items".
*/
export interface LibraryItem {
id: string;
itemType?: ('Textual' | 'Audio' | 'Video' | 'Game' | 'Other') | null;
@ -323,6 +346,10 @@ export interface LibraryItem {
createdAt: string;
_status?: ('draft' | 'published') | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "library-items-thumbnails".
*/
export interface LibraryItemThumbnail {
id: string;
libraryItem?: (string | LibraryItem)[] | null;
@ -361,6 +388,10 @@ export interface LibraryItemThumbnail {
};
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "library-items-gallery".
*/
export interface LibraryItemGallery {
id: string;
updatedAt: string;
@ -390,6 +421,10 @@ export interface LibraryItemGallery {
};
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "recorders".
*/
export interface Recorder {
id: string;
username: string;
@ -407,6 +442,10 @@ export interface Recorder {
lockUntil?: string | null;
password?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "recorders-thumbnails".
*/
export interface RecordersThumbnail {
id: string;
recorder?: (string | null) | Recorder;
@ -437,6 +476,10 @@ export interface RecordersThumbnail {
};
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "library-items-scans".
*/
export interface LibraryItemScans {
id: string;
updatedAt: string;
@ -482,6 +525,10 @@ export interface LibraryItemScans {
};
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "files".
*/
export interface File {
id: string;
filename: string;
@ -489,6 +536,10 @@ export interface File {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "keys".
*/
export interface Key {
id: string;
name: string;
@ -505,16 +556,22 @@ export interface Key {
| 'Wordings';
translations?: CategoryTranslations;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "currencies".
*/
export interface Currency {
id: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "contents".
*/
export interface Content {
id: string;
slug: string;
thumbnail?: string | ContentsThumbnail | null;
categories?: (string | Key)[] | null;
type?: (string | null) | Key;
libraryItems?: (string | LibraryItem)[] | null;
tags?: (string | Tag)[] | null;
translations: {
language: string | Language;
sourceLanguage: string | Language;
@ -603,7 +660,8 @@ export interface Content {
} | null;
id?: string | null;
}[];
folders?: (string | ContentsFolder)[] | null;
folders?: (string | Folder)[] | null;
libraryItems?: (string | LibraryItem)[] | null;
previousContents?: (string | Content)[] | null;
nextContents?: (string | Content)[] | null;
updatedBy: string | Recorder;
@ -611,6 +669,10 @@ export interface Content {
createdAt: string;
_status?: ('draft' | 'published') | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "contents-thumbnails".
*/
export interface ContentsThumbnail {
id: string;
contents?: (string | Content)[] | null;
@ -649,8 +711,13 @@ export interface ContentsThumbnail {
};
};
}
export interface ContentsFolder {
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "tags".
*/
export interface Tag {
id: string;
name?: string | null;
slug: string;
translations?:
| {
@ -659,9 +726,124 @@ export interface ContentsFolder {
id?: string | null;
}[]
| null;
subfolders?: (string | ContentsFolder)[] | null;
contents?: (string | Content)[] | null;
group: string | TagsGroup;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "tags-groups".
*/
export interface TagsGroup {
id: string;
slug: string;
icon?: string | null;
translations?:
| {
language: string | Language;
name: string;
id?: string | null;
}[]
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "pages".
*/
export interface Page {
id: string;
type: 'Content' | 'Article' | 'Generic';
slug: string;
thumbnail?: string | Image | null;
tags?: (string | Tag)[] | null;
authors?: (string | Recorder)[] | null;
translations: {
language: string | Language;
sourceLanguage: string | Language;
pretitle?: string | null;
title: string;
subtitle?: string | null;
summary?: {
root: {
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
type: string;
version: number;
};
[k: string]: unknown;
} | null;
content: {
root: {
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
type: string;
version: number;
};
[k: string]: unknown;
};
transcribers?: (string | Recorder)[] | null;
translators?: (string | Recorder)[] | null;
proofreaders?: (string | Recorder)[] | null;
id?: string | null;
}[];
folders?: (string | Folder)[] | null;
collectibles?: (string | LibraryItem)[] | null;
updatedBy: string | Recorder;
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "images".
*/
export interface Image {
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
filename?: string | null;
mimeType?: string | null;
filesize?: number | null;
width?: number | null;
height?: number | null;
sizes?: {
thumb?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
og?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "posts".
*/
export interface Post {
id: string;
slug: string;
@ -713,6 +895,10 @@ export interface Post {
createdAt: string;
_status?: ('draft' | 'published') | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "posts-thumbnails".
*/
export interface PostThumbnail {
id: string;
posts?: (string | Post)[] | null;
@ -751,6 +937,10 @@ export interface PostThumbnail {
};
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "chronology-items".
*/
export interface ChronologyItem {
id: string;
name?: string | null;
@ -806,6 +996,10 @@ export interface ChronologyItem {
createdAt: string;
_status?: ('draft' | 'published') | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "chronology-eras".
*/
export interface ChronologyEra {
id: string;
slug: string;
@ -837,6 +1031,10 @@ export interface ChronologyEra {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "weapons".
*/
export interface Weapon {
id: string;
slug: string;
@ -936,6 +1134,10 @@ export interface Weapon {
createdAt: string;
_status?: ('draft' | 'published') | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "weapons-thumbnails".
*/
export interface WeaponsThumbnail {
id: string;
weapon?: (string | null) | Weapon;
@ -982,6 +1184,10 @@ export interface WeaponsThumbnail {
};
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "weapons-groups".
*/
export interface WeaponsGroup {
id: string;
slug: string;
@ -994,6 +1200,10 @@ export interface WeaponsGroup {
| null;
weapons?: (string | Weapon)[] | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "notes".
*/
export interface Note {
id: string;
note: {
@ -1014,6 +1224,10 @@ export interface Note {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "videos".
*/
export interface Video {
id: string;
uid: string;
@ -1026,12 +1240,20 @@ export interface Video {
publishedDate: string;
channel?: (string | null) | VideosChannel;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "videos-channels".
*/
export interface VideosChannel {
id: string;
uid: string;
title: string;
subscribers?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: string;
user: {
@ -1051,6 +1273,10 @@ export interface PayloadPreference {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: string;
name?: string | null;
@ -1058,6 +1284,84 @@ export interface PayloadMigration {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "LineBlock".
*/
export interface LineBlock {
content: {
root: {
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
type: string;
version: number;
};
[k: string]: unknown;
};
blockType: 'lineBlock';
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "CueBlock".
*/
export interface CueBlock {
content: {
root: {
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
type: string;
version: number;
};
[k: string]: unknown;
};
blockType: 'cueBlock';
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "TranscriptBlock".
*/
export interface TranscriptBlock {
lines: (LineBlock | CueBlock)[];
id?: string | null;
blockName?: string | null;
blockType: 'transcriptBlock';
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "SectionBlock".
*/
export interface SectionBlock {
lines: {
root: {
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
type: string;
version: number;
};
[k: string]: unknown;
};
id?: string | null;
blockName?: string | null;
blockType: 'sectionBlock';
}
declare module 'payload' {

View File

@ -32,9 +32,12 @@ export const isValidPayloadImage = (
height?: number | null;
url?: string | null;
}
| undefined | null
| undefined
| null
| string
): image is PayloadImage => {
if (isUndefined(image)) return false;
if (typeof image === "string") return false;
if (isEmpty(image.filename)) return false;
if (isEmpty(image.url)) return false;
if (isEmpty(image.mimeType)) return false;
@ -49,5 +52,6 @@ export const isString = <T extends Object>(value: string | T): value is string =
export const isPayloadType = <T extends Object>(value: string | T): value is T =>
typeof value === "object";
export const isPayloadArrayType = <T extends Object>(value: string[] | T[]): value is T[] =>
value.every(isPayloadType<T>);
export const isPayloadArrayType = <T extends Object>(
value: (string | T)[] | null | undefined
): value is T[] => isDefined(value) && value.every(isPayloadType<T>);

29
src/utils/tags.ts Normal file
View File

@ -0,0 +1,29 @@
import { TagGroup } from "../sdk";
import { Tag } from "../types/collections";
import { isPayloadArrayType, isPayloadType } from "./asserts";
export const convertTagsToGroups = (tags: (string | Tag)[] | null | undefined): TagGroup[] => {
if (!isPayloadArrayType(tags)) {
return [];
}
const groups: TagGroup[] = [];
tags.forEach(({ group, slug }) => {
if (isPayloadType(group)) {
const existingGroup = groups.find((existingGroup) => existingGroup.slug === group.slug);
if (existingGroup) {
existingGroup.values.push(slug);
} else {
groups.push({
slug: group.slug,
icon: group.icon ?? "material-symbols:category-outline",
values: [slug],
});
}
}
});
return groups;
};