Added Categories
This commit is contained in:
parent
8b942f35e8
commit
138d7ae571
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"css.lint.unknownAtRules": "ignore",
|
||||
"editor.rulers": [100],
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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" },
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
};
|
|
@ -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>"
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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" }],
|
||||
}),
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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>
|
||||
);
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
|
@ -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: {
|
||||
|
|
|
@ -38,6 +38,7 @@ export interface Language {
|
|||
}
|
||||
export interface Recorder {
|
||||
id: string;
|
||||
avatar?: string | Image;
|
||||
username: string;
|
||||
anonymize: boolean;
|
||||
languages?: string[] | Language[];
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue