Added Categories

This commit is contained in:
DrMint 2023-07-14 16:42:37 +02:00
parent 8b942f35e8
commit 138d7ae571
16 changed files with 177 additions and 102 deletions

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"css.lint.unknownAtRules": "ignore",
"editor.rulers": [100],
"typescript.preferences.importModuleSpecifier": "non-relative",
}

View File

@ -12,7 +12,10 @@
"serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js",
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/",
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
"generate:graphQLSchema": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema"
"generate:graphQLSchema": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema",
"prettier": "prettier --list-different --end-of-line auto --write src",
"tsc": "tsc --noEmit",
"precommit": "npm run generate:types && npm run prettier && npm run tsc"
},
"dependencies": {
"cross-env": "^7.0.3",

22
prettier.config.js Normal file
View File

@ -0,0 +1,22 @@
/** @type {import("prettier").Options} */
module.exports = {
printWidth: 100,
tabWidth: 2,
useTabs: false,
semi: true,
singleQuote: false,
quoteProps: "as-needed",
jsxSingleQuote: false,
trailingComma: "es5",
bracketSpacing: true,
bracketSameLine: true,
arrowParens: "always",
rangeStart: 0,
rangeEnd: Infinity,
requirePragma: false,
insertPragma: false,
proseWrap: "preserve",
htmlWhitespaceSensitivity: "ignore",
endOfLine: "lf",
singleAttributePerLine: false,
};

View File

@ -0,0 +1,48 @@
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

@ -2,26 +2,25 @@ import { Props } from "payload/components/views/Cell";
import { useState, useEffect } from "react";
import React from "react";
export const ImageCell: React.FC<Props> = ({ cellData, field }) => {
export const ImageCell = ({ cellData, field }: Props): JSX.Element => {
const [imageURL, setImageURL] = useState<string>();
useEffect(() => {
const fetchUrl = async () => {
if (typeof cellData !== "string") return;
if (field.type !== "upload") return;
const result = await (
await fetch(`/api/${field.relationTo}/${cellData}`)
).json();
const result = await (await fetch(`/api/${field.relationTo}/${cellData}`)).json();
setImageURL(result.url);
};
fetchUrl();
}, [cellData]);
return imageURL ? (
<img
style={{ height: "3rem", borderRadius: "100%", aspectRatio: "1/1" }}
src={imageURL}
/>
) : (
"<No image>"
return (
<>
{imageURL ? (
<img style={{ height: "3rem", borderRadius: "100%", aspectRatio: "1/1" }} src={imageURL} />
) : (
"<No Image>"
)}
</>
);
};

View File

@ -1,11 +1,9 @@
import { CollectionConfig } from "payload/types";
import { localizedFields } from "../../utils/fields";
import { localizedFields } from "../../elements/translatedFields/translatedFields";
import { Languages } from "../Languages";
import { Images } from "../Images";
import { ImageCell } from "../../components/ImageCell";
import { BiographiesCell } from "./components/BiographiesCell";
import { Images } from "../Images/Images";
import { ImageCell } from "../Images/components/ImageCell";
import { beforeDuplicate } from "./hooks/beforeDuplicate";
import { BiographiesRowLabel } from "./components/BiographiesRowLabel";
const fields = {
username: "username",
@ -31,12 +29,7 @@ export const Recorders: CollectionConfig = {
hooks: { beforeDuplicate },
description:
"Recorders are contributors of the Accord's Library project. Create a Recorder here to be able to credit them in other collections",
defaultColumns: [
fields.username,
fields.anonymize,
fields.biographies,
fields.languages,
],
defaultColumns: [fields.username, fields.anonymize, fields.biographies, fields.languages],
},
timestamps: false,
fields: [
@ -87,8 +80,9 @@ export const Recorders: CollectionConfig = {
name: fields.biographies,
interfaceName: "RecorderBiographies",
admin: {
initCollapsed: true,
components: { RowLabel: BiographiesRowLabel, Cell: BiographiesCell },
useAsTitle: fields.biography,
description:
"A short personal description about you or your involvement with this project or the franchise",
},
fields: [{ name: fields.biography, type: "textarea" }],
}),

View File

@ -1,45 +0,0 @@
import { Props } from "payload/components/views/Cell";
import { useMemo } from "react";
import React from "react";
import { RecorderBiographies } from "../../../types/collections";
import { isDefined } from "../../../utils/asserts";
import { formatLanguageCode } from "../../../utils/string";
export const BiographiesCell: React.FC<Props> = ({ cellData }) => {
if (!Array.isArray(cellData)) return <>No biographies</>;
return (
<div style={{ display: "flex", gap: "6px" }}>
{cellData.map((biography: RecorderBiographies[number], index) => (
<BiographyCell key={biography.id} {...biography} index={index} />
))}
</div>
);
};
const BiographyCell: React.FC<
RecorderBiographies[number] & { index: number }
> = ({ language, biography, index }) => {
const label = useMemo(() => {
if (isDefined(language) && typeof language === "string") {
return formatLanguageCode(language);
}
return `Biography ${index}`;
}, []);
return (
<div
className="biography-cell"
style={{
backgroundColor: "var(--theme-elevation-100)",
color: "var(--theme-elevation-800)",
padding: "0.2em 0.5em",
borderRadius: 3,
}}
>
<abbr title={biography}>
<div style={{ position: "relative" }}>{label}</div>
</abbr>
</div>
);
};

View File

@ -1,19 +0,0 @@
import { RecorderBiographies } from "../../../types/collections";
import { AdminComponent } from "../../../utils/components";
import { isDefined } from "../../../utils/asserts";
import { formatLanguageCode, shortenEllipsis } from "../../../utils/string";
export const BiographiesRowLabel: AdminComponent<
RecorderBiographies[number]
> = ({ data: { language, biography }, index }) => {
const labelValues = [];
if (isDefined(language) && typeof language === "string") {
labelValues.push(formatLanguageCode(language));
}
if (isDefined(biography)) {
labelValues.push(shortenEllipsis(biography, 50));
}
const label = labelValues.join(" — ");
if (label === "") return `Biography ${index}`;
return label;
};

View File

@ -0,0 +1,24 @@
import React from "react";
import { Language } from "../../types/collections";
import { isDefined } from "../../utils/asserts";
import { formatLanguageCode } from "../../utils/string";
interface Props {
cellData: { language?: string | Language; title?: string }[];
}
export const Cell = ({ cellData }: Props): JSX.Element => (
<div style={{ display: "flex", gap: "6px" }}>
{cellData.map(({ language, title }, index) => (
<div key={index} className="pill">
<abbr title={title} style={{textDecorationColor: "var(--color-base-500)"}}>
<div style={{ position: "relative" }}>
{isDefined(language) && typeof language === "string"
? formatLanguageCode(language)
: index}
</div>
</abbr>
</div>
))}
</div>
);

View File

@ -0,0 +1,22 @@
import React from "react";
import { isDefined } from "../../utils/asserts";
import { formatLanguageCode, shortenEllipsis } from "../../utils/string";
import { Language } from "../../types/collections";
interface Props {
language?: Language | string;
title?: string;
}
export const RowLabel = ({ language, title }: Props): JSX.Element => {
return (
<div style={{ display: "flex", placeItems: "center", gap: 10 }}>
{isDefined(language) && typeof language === "string" && (
<div className="pill pill--style-white">{formatLanguageCode(language)}</div>
)}
{isDefined(title) && (
<div style={{ fontWeight: 600, fontSize: "1.2rem" }}>{shortenEllipsis(title, 50)}</div>
)}
</div>
);
};

View File

@ -1,20 +1,43 @@
import { ArrayField } from "payload/types";
import { hasDuplicates } from "./validation";
import { isDefined, isUndefined } from "./asserts";
import { Languages } from "../collections/Languages";
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";
type LocalizedFieldsProps = Omit<ArrayField, "type">;
type LocalizedFieldsProps = Omit<ArrayField, "type" | "admin"> & {
admin?: ArrayField["admin"] & { useAsTitle?: string };
};
type ArrayData = { [LANGUAGE_FIELD_NAME]?: string }[] | number | undefined;
export const localizedFields = ({
fields,
validate,
admin: { useAsTitle, ...admin },
...otherProps
}: LocalizedFieldsProps): ArrayField => ({
...otherProps,
type: "array",
admin: {
initCollapsed: true,
components: {
Cell: ({ cellData }) =>
Cell({
cellData: cellData.map((row) => ({
language: row.language,
title: isDefined(useAsTitle) ? row[useAsTitle] : undefined,
})),
}),
RowLabel: ({ data }) =>
RowLabel({
language: data.language,
title: isDefined(useAsTitle) ? data[useAsTitle] : undefined,
}),
},
...admin,
},
validate: (value, options) => {
const data = options.data[otherProps.name] as ArrayData;
if (isUndefined(data)) return true;

View File

@ -3,14 +3,15 @@ import path from "path";
import { Users } from "./collections/Users";
import { Languages } from "./collections/Languages";
import { Recorders } from "./collections/Recorders/Recorders";
import { Images } from "./collections/Images";
import { Images } from "./collections/Images/Images";
import { Categories } from "./collections/Categories/Categories";
export default buildConfig({
serverURL: "http://localhost:3000",
admin: {
user: Users.slug,
},
collections: [Users, Languages, Recorders, Images],
collections: [Users, Languages, Recorders, Images, Categories],
globals: [],
telemetry: false,
typescript: {

View File

@ -38,6 +38,7 @@ export interface Language {
}
export interface Recorder {
id: string;
avatar?: string | Image;
username: string;
anonymize: boolean;
languages?: string[] | Language[];

View File

@ -1,9 +1,7 @@
export const isDefined = <T>(value: T | null | undefined): value is T =>
value !== null && value !== undefined;
export const isUndefined = <T>(
value: T | null | undefined
): value is null | undefined => !isDefined(value);
export const isUndefined = <T>(value: T | null | undefined): value is null | undefined =>
!isDefined(value);
export const filterDefined = <T>(array: (T | null | undefined)[]): T[] =>
array.filter(isDefined);
export const filterDefined = <T>(array: (T | null | undefined)[]): T[] => array.filter(isDefined);

View File

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