Initial commit
This commit is contained in:
commit
8b942f35e8
|
@ -0,0 +1,170 @@
|
|||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
### Node Patch ###
|
||||
# Serverless Webpack directories
|
||||
.webpack/
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
# SvelteKit build / generate output
|
||||
.svelte-kit
|
||||
|
||||
### VisualStudioCode ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
### VisualStudioCode Patch ###
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
.ionide
|
||||
|
||||
# Support for Project snippet scope
|
||||
.vscode/*.code-snippets
|
||||
|
||||
# Ignore code-workspaces
|
||||
*.code-workspace
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/node,visualstudiocode
|
||||
|
||||
# Ignore Data
|
||||
mongo/
|
||||
uploads/
|
|
@ -0,0 +1,26 @@
|
|||
FROM node:18.8-alpine as base
|
||||
|
||||
FROM base as builder
|
||||
|
||||
WORKDIR /home/node/app
|
||||
COPY package*.json ./
|
||||
|
||||
COPY . .
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
FROM base as runtime
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV PAYLOAD_CONFIG_PATH=dist/payload.config.js
|
||||
|
||||
WORKDIR /home/node/app
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install --production
|
||||
COPY --from=builder /home/node/app/dist ./dist
|
||||
COPY --from=builder /home/node/app/build ./build
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "dist/server.js"]
|
|
@ -0,0 +1,19 @@
|
|||
# payload
|
||||
|
||||
This project was created using create-payload-app using the blank template.
|
||||
|
||||
## How to Use
|
||||
|
||||
`yarn dev` will start up your application and reload on any changes.
|
||||
|
||||
### Docker
|
||||
|
||||
If you have docker and docker-compose installed, you can run `docker-compose up`
|
||||
|
||||
To build the docker image, run `docker build -t my-tag .`
|
||||
|
||||
Ensure you are passing all needed environment variables when starting up your container via `--env-file` or setting them with your deployment.
|
||||
|
||||
The 3 typical env vars will be `MONGODB_URI`, `PAYLOAD_SECRET`, and `PAYLOAD_CONFIG_PATH`
|
||||
|
||||
`docker run --env-file .env -p 3000:3000 my-tag`
|
|
@ -0,0 +1,33 @@
|
|||
version: "3"
|
||||
|
||||
services:
|
||||
payload:
|
||||
image: node:18-alpine
|
||||
ports:
|
||||
- "${PAYLOAD_PORT}:${PAYLOAD_PORT}"
|
||||
volumes:
|
||||
- .:/home/node/app
|
||||
- node_modules:/home/node/app/node_modules
|
||||
working_dir: /home/node/app/
|
||||
command: sh -c "npm install && npm run dev"
|
||||
depends_on:
|
||||
- mongo
|
||||
environment:
|
||||
MONGODB_URI: ${MONGODB_URI}
|
||||
PAYLOAD_SECRET: ${PAYLOAD_SECRET}
|
||||
PORT: ${PAYLOAD_PORT}
|
||||
NODE_ENV: development
|
||||
|
||||
mongo:
|
||||
image: mongo:latest
|
||||
ports:
|
||||
- "${MONGODB_PORT}:27017"
|
||||
command:
|
||||
- --storageEngine=wiredTiger
|
||||
volumes:
|
||||
- ./mongo:/data/db
|
||||
logging:
|
||||
driver: none
|
||||
|
||||
volumes:
|
||||
node_modules:
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"ext": "ts",
|
||||
"exec": "ts-node src/server.ts"
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "payload",
|
||||
"description": "Payload project created from blank template",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/server.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon",
|
||||
"build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
|
||||
"build:server": "tsc",
|
||||
"build": "npm run copyfiles && npm run build:payload && npm run build:server",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"iso-639-1": "^2.1.15",
|
||||
"payload": "^1.11.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.9",
|
||||
"copyfiles": "^2.4.1",
|
||||
"nodemon": "^2.0.6",
|
||||
"prettier": "^3.0.0",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import { CollectionConfig } from "payload/types";
|
||||
|
||||
const fields = {
|
||||
filename: "filename",
|
||||
mimeType: "mimeType",
|
||||
filesize: "filesize",
|
||||
alt: "alt",
|
||||
} as const satisfies Record<string, string>;
|
||||
|
||||
const labels = {
|
||||
singular: "Image",
|
||||
plural: "Images",
|
||||
} as const satisfies { singular: string; plural: string };
|
||||
|
||||
export const Images: CollectionConfig = {
|
||||
slug: labels.plural,
|
||||
labels,
|
||||
typescript: { interface: labels.singular },
|
||||
defaultSort: fields.filename,
|
||||
admin: {
|
||||
useAsTitle: fields.filename,
|
||||
group: "Media",
|
||||
},
|
||||
|
||||
upload: {
|
||||
staticDir: `../uploads/${labels.plural}`,
|
||||
mimeTypes: ["image/*"],
|
||||
},
|
||||
|
||||
fields: [
|
||||
{
|
||||
name: fields.alt,
|
||||
label: "Alt Text",
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
import { CollectionConfig } from "payload/types";
|
||||
|
||||
const fields = {
|
||||
id: "id",
|
||||
name: "name",
|
||||
} as const satisfies Record<string, string>;
|
||||
|
||||
const labels = {
|
||||
singular: "Language",
|
||||
plural: "Languages",
|
||||
} as const satisfies { singular: string; plural: string };
|
||||
|
||||
export const Languages: CollectionConfig = {
|
||||
slug: labels.plural,
|
||||
labels,
|
||||
typescript: { interface: labels.singular },
|
||||
defaultSort: fields.name,
|
||||
admin: {
|
||||
useAsTitle: fields.name,
|
||||
defaultColumns: [fields.name, fields.id],
|
||||
},
|
||||
timestamps: false,
|
||||
fields: [
|
||||
{
|
||||
name: fields.id,
|
||||
type: "text",
|
||||
unique: true,
|
||||
required: true,
|
||||
validate: (value) => {
|
||||
if (/^[a-z]{2}(-[a-z]{2})?$/g.test(value)) {
|
||||
return true;
|
||||
}
|
||||
return "The code must be a valid IETF language tag and lowercase (i.e: en, pt-pt, fr, zh-tw...)";
|
||||
},
|
||||
},
|
||||
{
|
||||
name: fields.name,
|
||||
type: "text",
|
||||
unique: true,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
};
|
|
@ -0,0 +1,96 @@
|
|||
import { CollectionConfig } from "payload/types";
|
||||
import { localizedFields } from "../../utils/fields";
|
||||
import { Languages } from "../Languages";
|
||||
import { Images } from "../Images";
|
||||
import { ImageCell } from "../../components/ImageCell";
|
||||
import { BiographiesCell } from "./components/BiographiesCell";
|
||||
import { beforeDuplicate } from "./hooks/beforeDuplicate";
|
||||
import { BiographiesRowLabel } from "./components/BiographiesRowLabel";
|
||||
|
||||
const fields = {
|
||||
username: "username",
|
||||
anonymize: "anonymize",
|
||||
languages: "languages",
|
||||
biographies: "biographies",
|
||||
biography: "biography",
|
||||
avatar: "avatar",
|
||||
} as const satisfies Record<string, string>;
|
||||
|
||||
const labels = {
|
||||
singular: "Recorder",
|
||||
plural: "Recorders",
|
||||
} as const satisfies { singular: string; plural: string };
|
||||
|
||||
export const Recorders: CollectionConfig = {
|
||||
slug: labels.plural,
|
||||
labels,
|
||||
typescript: { interface: labels.singular },
|
||||
defaultSort: fields.username,
|
||||
admin: {
|
||||
useAsTitle: fields.username,
|
||||
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,
|
||||
],
|
||||
},
|
||||
timestamps: false,
|
||||
fields: [
|
||||
{
|
||||
type: "row",
|
||||
fields: [
|
||||
{
|
||||
name: fields.avatar,
|
||||
type: "upload",
|
||||
relationTo: Images.slug,
|
||||
admin: {
|
||||
components: {
|
||||
Cell: ImageCell,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: fields.username,
|
||||
type: "text",
|
||||
unique: true,
|
||||
required: true,
|
||||
admin: { description: "The username must be unique" },
|
||||
},
|
||||
{
|
||||
name: fields.anonymize,
|
||||
type: "checkbox",
|
||||
required: true,
|
||||
defaultValue: false,
|
||||
admin: {
|
||||
width: "50%",
|
||||
description:
|
||||
"If enabled, this recorder's username will not be made public. Instead they will be referred to as 'Recorder#0000' where '0000' is a random four digit number",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: fields.languages,
|
||||
type: "relationship",
|
||||
relationTo: Languages.slug,
|
||||
hasMany: true,
|
||||
admin: {
|
||||
allowCreate: false,
|
||||
description: "List of language(s) that this recorder is familiar with",
|
||||
},
|
||||
},
|
||||
localizedFields({
|
||||
name: fields.biographies,
|
||||
interfaceName: "RecorderBiographies",
|
||||
admin: {
|
||||
initCollapsed: true,
|
||||
components: { RowLabel: BiographiesRowLabel, Cell: BiographiesCell },
|
||||
},
|
||||
fields: [{ name: fields.biography, type: "textarea" }],
|
||||
}),
|
||||
],
|
||||
};
|
|
@ -0,0 +1,45 @@
|
|||
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>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
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,9 @@
|
|||
import { BeforeDuplicate } from "payload/types";
|
||||
import { Recorder } from "../../../types/collections";
|
||||
|
||||
export const beforeDuplicate: BeforeDuplicate<Recorder> = ({ data }) => {
|
||||
return {
|
||||
...data,
|
||||
id: `${data.id}-copy`,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
import { CollectionConfig } from "payload/types";
|
||||
|
||||
const fields = {
|
||||
email: "email",
|
||||
} as const satisfies Record<string, string>;
|
||||
|
||||
const labels = {
|
||||
singular: "User",
|
||||
plural: "Users",
|
||||
} as const satisfies { singular: string; plural: string };
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: labels.plural,
|
||||
auth: true,
|
||||
labels,
|
||||
typescript: { interface: labels.singular },
|
||||
defaultSort: fields.email,
|
||||
admin: {
|
||||
useAsTitle: fields.email,
|
||||
defaultColumns: [fields.email],
|
||||
group: "Administration",
|
||||
},
|
||||
timestamps: false,
|
||||
fields: [],
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
import { Props } from "payload/components/views/Cell";
|
||||
import { useState, useEffect } from "react";
|
||||
import React from "react";
|
||||
|
||||
export const ImageCell: React.FC<Props> = ({ cellData, field }) => {
|
||||
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();
|
||||
setImageURL(result.url);
|
||||
};
|
||||
fetchUrl();
|
||||
}, [cellData]);
|
||||
|
||||
return imageURL ? (
|
||||
<img
|
||||
style={{ height: "3rem", borderRadius: "100%", aspectRatio: "1/1" }}
|
||||
src={imageURL}
|
||||
/>
|
||||
) : (
|
||||
"<No image>"
|
||||
);
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
import { buildConfig } from "payload/config";
|
||||
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";
|
||||
|
||||
export default buildConfig({
|
||||
serverURL: "http://localhost:3000",
|
||||
admin: {
|
||||
user: Users.slug,
|
||||
},
|
||||
collections: [Users, Languages, Recorders, Images],
|
||||
globals: [],
|
||||
telemetry: false,
|
||||
typescript: {
|
||||
outputFile: path.resolve(__dirname, "types/collections.ts"),
|
||||
},
|
||||
graphQL: {
|
||||
schemaOutputFile: path.resolve(__dirname, "generated-schema.graphql"),
|
||||
},
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
import express from "express";
|
||||
import payload from "payload";
|
||||
|
||||
require("dotenv").config();
|
||||
const app = express();
|
||||
|
||||
// Redirect root to Admin panel
|
||||
app.get("/", (_, res) => {
|
||||
res.redirect("/admin");
|
||||
});
|
||||
|
||||
const start = async () => {
|
||||
// Initialize Payload
|
||||
await payload.init({
|
||||
secret: process.env.PAYLOAD_SECRET,
|
||||
mongoURL: process.env.MONGODB_URI,
|
||||
express: app,
|
||||
onInit: async () => {
|
||||
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`);
|
||||
},
|
||||
});
|
||||
|
||||
// Add your own express routes here
|
||||
|
||||
app.listen(3000);
|
||||
};
|
||||
|
||||
start();
|
|
@ -0,0 +1,57 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
export type RecorderBiographies = {
|
||||
language: string | Language;
|
||||
biography?: string;
|
||||
id?: string;
|
||||
}[];
|
||||
|
||||
export interface Config {
|
||||
collections: {
|
||||
Users: User;
|
||||
Languages: Language;
|
||||
Recorders: Recorder;
|
||||
Images: Image;
|
||||
};
|
||||
globals: {};
|
||||
}
|
||||
export interface User {
|
||||
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;
|
||||
username: string;
|
||||
anonymize: boolean;
|
||||
languages?: string[] | Language[];
|
||||
biographies?: RecorderBiographies;
|
||||
}
|
||||
export interface Image {
|
||||
id: string;
|
||||
alt?: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string;
|
||||
filename?: string;
|
||||
mimeType?: string;
|
||||
filesize?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
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 filterDefined = <T>(array: (T | null | undefined)[]): T[] =>
|
||||
array.filter(isDefined);
|
|
@ -0,0 +1,4 @@
|
|||
export type AdminComponent<T extends Record<string, any>> = (props: {
|
||||
data: Partial<T>;
|
||||
index?: number;
|
||||
}) => string;
|
|
@ -0,0 +1,40 @@
|
|||
import { ArrayField } from "payload/types";
|
||||
import { hasDuplicates } from "./validation";
|
||||
import { isDefined, isUndefined } from "./asserts";
|
||||
import { Languages } from "../collections/Languages";
|
||||
|
||||
const LANGUAGE_FIELD_NAME = "language";
|
||||
|
||||
type LocalizedFieldsProps = Omit<ArrayField, "type">;
|
||||
type ArrayData = { [LANGUAGE_FIELD_NAME]?: string }[] | number | undefined;
|
||||
|
||||
export const localizedFields = ({
|
||||
fields,
|
||||
validate,
|
||||
...otherProps
|
||||
}: LocalizedFieldsProps): ArrayField => ({
|
||||
...otherProps,
|
||||
type: "array",
|
||||
validate: (value, options) => {
|
||||
const data = options.data[otherProps.name] as ArrayData;
|
||||
if (isUndefined(data)) return true;
|
||||
if (typeof data === "number") return true;
|
||||
|
||||
const languages = data.map((biography) => biography.language);
|
||||
if (hasDuplicates(languages)) {
|
||||
return `There cannot be multiple ${otherProps.name} with the same ${LANGUAGE_FIELD_NAME}`;
|
||||
}
|
||||
|
||||
return isDefined(validate) ? validate(value, options) : true;
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: LANGUAGE_FIELD_NAME,
|
||||
type: "relationship",
|
||||
relationTo: Languages.slug,
|
||||
required: true,
|
||||
admin: { allowCreate: false },
|
||||
},
|
||||
...fields,
|
||||
],
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
import ISO6391 from "iso-639-1";
|
||||
|
||||
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;
|
|
@ -0,0 +1,2 @@
|
|||
export const hasDuplicates = <T>(list: T[]): boolean =>
|
||||
list.length !== new Set(list).size;
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"strict": false,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"jsx": "react",
|
||||
"paths": {
|
||||
"payload/generated-types": [
|
||||
"./src/payload-types.ts",
|
||||
],
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"build",
|
||||
],
|
||||
"ts-node": {
|
||||
"transpileOnly": true,
|
||||
"swc": true,
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue