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