Clone starter
This commit is contained in:
1
apps/api/.dockerignore
Normal file
1
apps/api/.dockerignore
Normal file
@ -0,0 +1 @@
|
||||
**/node_modules
|
5
apps/api/.eslintrc.cjs
Normal file
5
apps/api/.eslintrc.cjs
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["custom"],
|
||||
rules: {},
|
||||
};
|
169
apps/api/.gitignore
vendored
Normal file
169
apps/api/.gitignore
vendored
Normal file
@ -0,0 +1,169 @@
|
||||
### 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
|
||||
|
||||
# Local media files
|
||||
media
|
39
apps/api/Dockerfile
Normal file
39
apps/api/Dockerfile
Normal file
@ -0,0 +1,39 @@
|
||||
FROM node:18-alpine as base
|
||||
RUN npm i -g pnpm turbo
|
||||
|
||||
FROM base AS pruner
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN turbo prune --scope=@turbopress/api --docker
|
||||
|
||||
# remove all empty node_modules folder structure
|
||||
RUN rm -rf /app/out/full/*/*/node_modules
|
||||
|
||||
FROM base AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN apk update
|
||||
WORKDIR /app
|
||||
|
||||
# Add lockfile and package.json's of isolated subworkspace
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=pruner /app/out/json/ .
|
||||
RUN pnpm install
|
||||
|
||||
# Build the project and its dependencies
|
||||
COPY --from=pruner /app/out/full/ .
|
||||
ENV PAYLOAD_CONFIG_PATH=src/payload.config.ts
|
||||
RUN pnpm build:api
|
||||
|
||||
# Run App
|
||||
FROM base as runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
ENV PAYLOAD_CONFIG_PATH=dist/payload.config.js
|
||||
COPY --from=builder /app/apps/api/package.json .
|
||||
RUN pnpm install --prod
|
||||
COPY --from=builder /app/apps/api/dist ./dist
|
||||
COPY --from=builder /app/apps/api/build ./build
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "dist/server.js"]
|
19
apps/api/README.md
Normal file
19
apps/api/README.md
Normal file
@ -0,0 +1,19 @@
|
||||
# api
|
||||
|
||||
This project was created using create-payload-app using the blog 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 -f Dockerfile ../..`
|
||||
|
||||
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`
|
5
apps/api/nodemon.json
Normal file
5
apps/api/nodemon.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"watch": ["src"],
|
||||
"ext": "ts",
|
||||
"exec": "ts-node src/server.ts"
|
||||
}
|
36
apps/api/package.json
Normal file
36
apps/api/package.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@turbopress/api",
|
||||
"description": "Headless CMS based on Payload",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/server.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "nodemon",
|
||||
"build:payload": "payload build",
|
||||
"build:server": "tsc",
|
||||
"build": "pnpm copyfiles && pnpm build:payload && pnpm build:server",
|
||||
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/",
|
||||
"generate:types": "payload generate:types",
|
||||
"lint": "eslint \"./src/**/*.{js,ts}\" --fix",
|
||||
"serve": "PAYLOAD_CONFIG_PATH=dist/payload.config.js node dist/server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"payload": "latest",
|
||||
"@payloadcms/plugin-seo": "latest",
|
||||
"@payloadcms/plugin-cloud-storage": "latest",
|
||||
"@aws-sdk/client-s3": "latest",
|
||||
"@aws-sdk/lib-storage": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "latest",
|
||||
"@types/express": "^4.17.9",
|
||||
"copyfiles": "^2.4.1",
|
||||
"nodemon": "^2.0.6",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.8.4",
|
||||
"webpack-hot-middleware": "^2.25.4",
|
||||
"eslint-config-custom": "*"
|
||||
}
|
||||
}
|
128
apps/api/src/blocks/Menu.ts
Normal file
128
apps/api/src/blocks/Menu.ts
Normal file
@ -0,0 +1,128 @@
|
||||
import payload from "payload";
|
||||
import { Block } from "payload/types";
|
||||
import Pages from "../collections/Pages";
|
||||
import linkField from "../fields/linkField";
|
||||
|
||||
export const Menu: Block = {
|
||||
slug: "menu",
|
||||
interfaceName: "Menu",
|
||||
fields: [
|
||||
{
|
||||
name: "type",
|
||||
type: "select",
|
||||
options: ["default"],
|
||||
required: true,
|
||||
defaultValue: "default",
|
||||
},
|
||||
{
|
||||
name: "menus",
|
||||
type: "array",
|
||||
fields: [
|
||||
{
|
||||
name: "mainMenu",
|
||||
type: "group",
|
||||
interfaceName: "MainMenu",
|
||||
fields: [
|
||||
{
|
||||
type: "row",
|
||||
fields: [
|
||||
{
|
||||
name: "type",
|
||||
type: "radio",
|
||||
options: [
|
||||
{
|
||||
label: "Internal link",
|
||||
value: "reference",
|
||||
},
|
||||
{
|
||||
label: "Custom URL",
|
||||
value: "custom",
|
||||
},
|
||||
{
|
||||
label: "None",
|
||||
value: "none",
|
||||
},
|
||||
],
|
||||
defaultValue: "reference",
|
||||
admin: {
|
||||
layout: "horizontal",
|
||||
width: "50%",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "newTab",
|
||||
label: "Open in new tab",
|
||||
type: "checkbox",
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData?.type != "none",
|
||||
width: "50%",
|
||||
style: {
|
||||
alignSelf: "flex-end",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reference",
|
||||
label: "Document to link to",
|
||||
type: "relationship",
|
||||
relationTo: [Pages.slug],
|
||||
required: true,
|
||||
maxDepth: 0,
|
||||
admin: {
|
||||
condition: (_, siblingData) =>
|
||||
siblingData?.type === "reference",
|
||||
width: "50%",
|
||||
},
|
||||
hooks: {
|
||||
afterRead: [
|
||||
async ({ value, siblingData }) => {
|
||||
if (value && siblingData.type === "reference") {
|
||||
const id = value.value;
|
||||
const pages = await payload.find({
|
||||
collection: "pages",
|
||||
where: {
|
||||
id: { equals: id },
|
||||
},
|
||||
depth: 0,
|
||||
});
|
||||
|
||||
if (pages.docs[0]?.slug)
|
||||
siblingData.url = pages.docs[0].slug;
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "url",
|
||||
label: "Custom URL",
|
||||
type: "text",
|
||||
required: true,
|
||||
admin: {
|
||||
condition: (_, siblingData) =>
|
||||
siblingData?.type === "custom",
|
||||
width: "50%",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "label",
|
||||
label: "Label",
|
||||
type: "text",
|
||||
required: true,
|
||||
admin: {
|
||||
width: "50%",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "subMenu",
|
||||
type: "array",
|
||||
fields: [linkField()],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
17
apps/api/src/blocks/PageContent.ts
Normal file
17
apps/api/src/blocks/PageContent.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Block } from "payload/types";
|
||||
|
||||
export const PageContent: Block = {
|
||||
slug: "pageContent",
|
||||
interfaceName: "PageContent",
|
||||
fields: [
|
||||
{
|
||||
name: "description",
|
||||
type: "textarea",
|
||||
defaultValue:
|
||||
"This block will display the content of the page (if any). Please edit the original page change the value.",
|
||||
admin: {
|
||||
readOnly: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
52
apps/api/src/blocks/PageList.ts
Normal file
52
apps/api/src/blocks/PageList.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { Block } from "payload/types";
|
||||
import Categories from "../collections/Categories";
|
||||
import { PagesField } from "../collections/Pages";
|
||||
import Tags from "../collections/Tags";
|
||||
|
||||
const PageListField = {
|
||||
numberOfItems: "numberOfItems",
|
||||
filterByCategories: "filterByCategories",
|
||||
filterByTags: "filterByTags",
|
||||
sortBy: "sortBy",
|
||||
pages: "pages",
|
||||
};
|
||||
|
||||
type PageListField = (typeof PageListField)[keyof typeof PageListField];
|
||||
|
||||
export const PageList: Block = {
|
||||
slug: "pageList",
|
||||
interfaceName: "PageList",
|
||||
fields: [
|
||||
{
|
||||
name: PageListField.numberOfItems,
|
||||
type: "number",
|
||||
defaultValue: 5,
|
||||
},
|
||||
{
|
||||
name: PageListField.filterByCategories,
|
||||
type: "relationship",
|
||||
relationTo: [Categories.slug],
|
||||
maxDepth: 0,
|
||||
hasMany: true,
|
||||
},
|
||||
{
|
||||
name: PageListField.filterByTags,
|
||||
type: "relationship",
|
||||
relationTo: [Tags.slug],
|
||||
hasMany: true,
|
||||
maxDepth: 0,
|
||||
},
|
||||
{
|
||||
name: PageListField.sortBy,
|
||||
type: "select",
|
||||
options: [
|
||||
PagesField.title,
|
||||
PagesField.createdAt,
|
||||
PagesField.updatedAt,
|
||||
`-${PagesField.title}`,
|
||||
`-${PagesField.createdAt}`,
|
||||
`-${PagesField.updatedAt}`,
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
15
apps/api/src/blocks/ReusableContent.ts
Normal file
15
apps/api/src/blocks/ReusableContent.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Block } from "payload/types";
|
||||
import Contents from "../collections/Contents";
|
||||
|
||||
export const ReusableContent: Block = {
|
||||
slug: "reusableContent",
|
||||
interfaceName: "ReusableContent",
|
||||
fields: [
|
||||
{
|
||||
name: "reference",
|
||||
type: "relationship",
|
||||
// maxDepth: 0,
|
||||
relationTo: [Contents.slug],
|
||||
},
|
||||
],
|
||||
};
|
9
apps/api/src/blocks/SiteTitle.ts
Normal file
9
apps/api/src/blocks/SiteTitle.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { Block } from "payload/types";
|
||||
|
||||
export const SiteTitle: Block = {
|
||||
slug: "siteTitle",
|
||||
interfaceName: "SiteTitle",
|
||||
fields: [
|
||||
{ name: "siteName", type: "text", required: true, admin: { width: "50%" } },
|
||||
],
|
||||
};
|
37
apps/api/src/collections/Categories.ts
Normal file
37
apps/api/src/collections/Categories.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import type { CollectionConfig } from "payload/types";
|
||||
|
||||
const CategoriesField = {
|
||||
name: "name",
|
||||
slug: "slug",
|
||||
};
|
||||
|
||||
type CategoriesField = (typeof CategoriesField)[keyof typeof CategoriesField];
|
||||
|
||||
const Categories: CollectionConfig = {
|
||||
slug: "categories",
|
||||
admin: {
|
||||
useAsTitle: CategoriesField.name,
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: CategoriesField.name,
|
||||
type: "text",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: CategoriesField.slug,
|
||||
type: "text",
|
||||
required: true,
|
||||
unique: true,
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
},
|
||||
},
|
||||
],
|
||||
timestamps: false,
|
||||
};
|
||||
|
||||
export default Categories;
|
48
apps/api/src/collections/Contents.ts
Normal file
48
apps/api/src/collections/Contents.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { CollectionConfig } from "payload/types";
|
||||
import { Menu } from "../blocks/Menu";
|
||||
import { PageContent } from "../blocks/PageContent";
|
||||
import { PageList } from "../blocks/PageList";
|
||||
import { SiteTitle } from "../blocks/SiteTitle";
|
||||
|
||||
const ContentsField = {
|
||||
name: "name",
|
||||
slug: "slug",
|
||||
description: "description",
|
||||
};
|
||||
type ContentsField = (typeof ContentsField)[keyof typeof ContentsField];
|
||||
|
||||
const Contents: CollectionConfig = {
|
||||
slug: "contents",
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: ContentsField.name,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: ContentsField.name,
|
||||
type: "text",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: ContentsField.slug,
|
||||
type: "text",
|
||||
unique: true,
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ContentsField.description,
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
name: "blocks",
|
||||
type: "blocks",
|
||||
blocks: [Menu, PageContent, PageList, SiteTitle],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default Contents;
|
79
apps/api/src/collections/Layouts.ts
Normal file
79
apps/api/src/collections/Layouts.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { CollectionConfig } from "payload/types";
|
||||
import { Menu } from "../blocks/Menu";
|
||||
import { PageContent } from "../blocks/PageContent";
|
||||
import { PageList } from "../blocks/PageList";
|
||||
import { ReusableContent } from "../blocks/ReusableContent";
|
||||
import { SiteTitle } from "../blocks/SiteTitle";
|
||||
|
||||
const LayoutsField = {
|
||||
name: "name",
|
||||
slug: "slug",
|
||||
description: "description",
|
||||
};
|
||||
type LayoutsField = (typeof LayoutsField)[keyof typeof LayoutsField];
|
||||
|
||||
const blocks = [ReusableContent];
|
||||
|
||||
const Layouts: CollectionConfig = {
|
||||
slug: "layouts",
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: LayoutsField.name,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: LayoutsField.name,
|
||||
type: "text",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: LayoutsField.slug,
|
||||
type: "text",
|
||||
unique: true,
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: LayoutsField.description,
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
name: "header",
|
||||
type: "group",
|
||||
fields: [
|
||||
{
|
||||
name: "blocks",
|
||||
type: "blocks",
|
||||
blocks: [...blocks, Menu, SiteTitle],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "body",
|
||||
type: "group",
|
||||
fields: [
|
||||
{
|
||||
name: "blocks",
|
||||
type: "blocks",
|
||||
blocks: [...blocks, PageContent, PageList],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "footer",
|
||||
type: "group",
|
||||
fields: [
|
||||
{
|
||||
name: "blocks",
|
||||
type: "blocks",
|
||||
blocks: blocks,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default Layouts;
|
29
apps/api/src/collections/Media.ts
Normal file
29
apps/api/src/collections/Media.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import type { CollectionConfig } from "payload/types";
|
||||
|
||||
const Media: CollectionConfig = {
|
||||
slug: "media",
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
upload: {
|
||||
disableLocalStorage: true,
|
||||
adminThumbnail: "thumbnail",
|
||||
imageSizes: [
|
||||
{
|
||||
height: 400,
|
||||
width: 400,
|
||||
crop: "center",
|
||||
name: "thumbnail",
|
||||
},
|
||||
{
|
||||
width: 900,
|
||||
height: 450,
|
||||
crop: "center",
|
||||
name: "sixteenByNineMedium",
|
||||
},
|
||||
],
|
||||
},
|
||||
fields: [],
|
||||
};
|
||||
|
||||
export default Media;
|
113
apps/api/src/collections/Pages.ts
Normal file
113
apps/api/src/collections/Pages.ts
Normal file
@ -0,0 +1,113 @@
|
||||
import type { CollectionConfig } from "payload/types";
|
||||
|
||||
export const PagesField = {
|
||||
title: "title",
|
||||
slug: "slug",
|
||||
author: "author",
|
||||
publishedDate: "publishedDate",
|
||||
categories: "categories",
|
||||
tags: "tags",
|
||||
content: "content",
|
||||
status: "status",
|
||||
layout: "layout",
|
||||
createdAt: "createdAt",
|
||||
updatedAt: "updatedAt",
|
||||
};
|
||||
|
||||
type PagesField = (typeof PagesField)[keyof typeof PagesField];
|
||||
|
||||
const PagesFieldStatus = {
|
||||
Draft: "Draft",
|
||||
Published: "Published",
|
||||
};
|
||||
|
||||
type PagesFieldStatus =
|
||||
(typeof PagesFieldStatus)[keyof typeof PagesFieldStatus];
|
||||
|
||||
const Pages: CollectionConfig = {
|
||||
slug: "pages",
|
||||
admin: {
|
||||
defaultColumns: [
|
||||
PagesField.title,
|
||||
PagesField.slug,
|
||||
PagesField.author,
|
||||
PagesField.categories,
|
||||
PagesField.tags,
|
||||
PagesField.status,
|
||||
],
|
||||
useAsTitle: PagesField.title,
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: PagesField.title,
|
||||
type: "text",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: PagesField.slug,
|
||||
type: "text",
|
||||
required: true,
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: PagesField.author,
|
||||
type: "relationship",
|
||||
relationTo: "users",
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: PagesField.publishedDate,
|
||||
type: "date",
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: PagesField.categories,
|
||||
type: "relationship",
|
||||
relationTo: "categories",
|
||||
hasMany: true,
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: PagesField.tags,
|
||||
type: "relationship",
|
||||
relationTo: "tags",
|
||||
hasMany: true,
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: PagesField.status,
|
||||
type: "select",
|
||||
options: Object.entries(PagesFieldStatus).map((e) => {
|
||||
return { label: e[0], value: e[1] };
|
||||
}),
|
||||
defaultValue: PagesFieldStatus.Draft,
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: PagesField.layout,
|
||||
type: "relationship",
|
||||
relationTo: "layouts",
|
||||
},
|
||||
{
|
||||
name: PagesField.content,
|
||||
type: "richText",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default Pages;
|
37
apps/api/src/collections/Tags.ts
Normal file
37
apps/api/src/collections/Tags.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import type { CollectionConfig } from "payload/types";
|
||||
|
||||
const TagsField = {
|
||||
name: "name",
|
||||
slug: "slug",
|
||||
};
|
||||
|
||||
type TagsField = (typeof TagsField)[keyof typeof TagsField];
|
||||
|
||||
const Tags: CollectionConfig = {
|
||||
slug: "tags",
|
||||
admin: {
|
||||
useAsTitle: TagsField.name,
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: TagsField.name,
|
||||
type: "text",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: TagsField.slug,
|
||||
type: "text",
|
||||
required: true,
|
||||
unique: true,
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
},
|
||||
},
|
||||
],
|
||||
timestamps: false,
|
||||
};
|
||||
|
||||
export default Tags;
|
18
apps/api/src/collections/Users.ts
Normal file
18
apps/api/src/collections/Users.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import type { CollectionConfig } from "payload/types";
|
||||
|
||||
const Users: CollectionConfig = {
|
||||
slug: "users",
|
||||
auth: true,
|
||||
admin: {
|
||||
useAsTitle: "email",
|
||||
},
|
||||
fields: [
|
||||
// Email added by default
|
||||
{
|
||||
name: "name",
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default Users;
|
15
apps/api/src/fields/enableField.ts
Normal file
15
apps/api/src/fields/enableField.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { SelectField } from "payload/types";
|
||||
|
||||
function enableField(fieldOverrides?: Partial<SelectField>): SelectField {
|
||||
return {
|
||||
label: "Enable",
|
||||
name: "enable",
|
||||
type: "select",
|
||||
options: ["Yes", "No"],
|
||||
defaultValue: "Yes",
|
||||
required: true,
|
||||
...fieldOverrides,
|
||||
};
|
||||
}
|
||||
|
||||
export default enableField;
|
100
apps/api/src/fields/linkField.ts
Normal file
100
apps/api/src/fields/linkField.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import payload from "payload";
|
||||
import { GroupField } from "payload/types";
|
||||
|
||||
function linkField(fieldOverrides?: Partial<GroupField>): GroupField {
|
||||
return {
|
||||
name: "link",
|
||||
type: "group",
|
||||
interfaceName: "Link",
|
||||
fields: [
|
||||
{
|
||||
type: "row",
|
||||
fields: [
|
||||
{
|
||||
name: "type",
|
||||
type: "radio",
|
||||
options: [
|
||||
{
|
||||
label: "Internal link",
|
||||
value: "reference",
|
||||
},
|
||||
{
|
||||
label: "Custom URL",
|
||||
value: "custom",
|
||||
},
|
||||
],
|
||||
defaultValue: "reference",
|
||||
admin: {
|
||||
layout: "horizontal",
|
||||
width: "50%",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "newTab",
|
||||
label: "Open in new tab",
|
||||
type: "checkbox",
|
||||
admin: {
|
||||
width: "50%",
|
||||
style: {
|
||||
alignSelf: "flex-end",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reference",
|
||||
label: "Document to link to",
|
||||
type: "relationship",
|
||||
relationTo: ["pages"],
|
||||
required: true,
|
||||
maxDepth: 0,
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData?.type === "reference",
|
||||
width: "50%",
|
||||
},
|
||||
hooks: {
|
||||
afterRead: [
|
||||
async ({ value, siblingData }) => {
|
||||
if (value && siblingData.type === "reference") {
|
||||
const id = value.value;
|
||||
const pages = await payload.find({
|
||||
collection: "pages",
|
||||
where: {
|
||||
id: { equals: id },
|
||||
},
|
||||
depth: 0,
|
||||
});
|
||||
|
||||
if (pages.docs[0]?.slug)
|
||||
if (pages.docs[0]) siblingData.url = pages.docs[0].slug;
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "url",
|
||||
label: "Custom URL",
|
||||
type: "text",
|
||||
required: true,
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData?.type === "custom",
|
||||
width: "50%",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "label",
|
||||
label: "Label",
|
||||
type: "text",
|
||||
required: true,
|
||||
admin: {
|
||||
width: "50%",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
...fieldOverrides,
|
||||
};
|
||||
}
|
||||
|
||||
export default linkField;
|
59
apps/api/src/payload.config.ts
Normal file
59
apps/api/src/payload.config.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { cloudStorage } from "@payloadcms/plugin-cloud-storage";
|
||||
import { s3Adapter } from "@payloadcms/plugin-cloud-storage/s3";
|
||||
import seo from "@payloadcms/plugin-seo";
|
||||
import { GenerateTitle } from "@payloadcms/plugin-seo/dist/types";
|
||||
import path from "path";
|
||||
import { buildConfig } from "payload/config";
|
||||
import Categories from "./collections/Categories";
|
||||
import Contents from "./collections/Contents";
|
||||
import Layouts from "./collections/Layouts";
|
||||
import Media from "./collections/Media";
|
||||
import Pages from "./collections/Pages";
|
||||
import Tags from "./collections/Tags";
|
||||
import Users from "./collections/Users";
|
||||
const generateTitle: GenerateTitle = ({ slug, doc }) => {
|
||||
let title = "TurboPress";
|
||||
if (slug == "pages") {
|
||||
const page = doc as any;
|
||||
return (title = `TurboPress - ${page?.title?.value}`);
|
||||
}
|
||||
return title;
|
||||
};
|
||||
|
||||
const adapter = s3Adapter({
|
||||
config: {
|
||||
credentials: {
|
||||
accessKeyId: process.env.S3_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
|
||||
},
|
||||
region: process.env.S3_REGION,
|
||||
endpoint: process.env.S3_ENDPOINT,
|
||||
},
|
||||
bucket: process.env.S3_BUCKET,
|
||||
});
|
||||
|
||||
export default buildConfig({
|
||||
serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL ?? "http://localhost:3000",
|
||||
admin: {
|
||||
user: Users.slug,
|
||||
},
|
||||
collections: [Categories, Contents, Layouts, Media, Pages, Tags, Users],
|
||||
typescript: {
|
||||
outputFile: path.join(__dirname, "../types", "payload.ts"),
|
||||
},
|
||||
plugins: [
|
||||
seo({
|
||||
collections: ["pages"],
|
||||
uploadsCollection: "media",
|
||||
generateTitle: generateTitle,
|
||||
}),
|
||||
cloudStorage({
|
||||
collections: {
|
||||
media: {
|
||||
adapter: adapter,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
cors: "*",
|
||||
});
|
30
apps/api/src/server.ts
Normal file
30
apps/api/src/server.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import express from "express";
|
||||
import payload from "payload";
|
||||
|
||||
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()}`);
|
||||
},
|
||||
mongoOptions: {
|
||||
dbName: process.env.DB_NAME,
|
||||
},
|
||||
});
|
||||
|
||||
// Add your own express routes here
|
||||
|
||||
app.listen(process.env.PAYLOAD_PORT ?? 3000);
|
||||
};
|
||||
|
||||
start();
|
22
apps/api/tsconfig.json
Normal file
22
apps/api/tsconfig.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"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": ["./types/payload.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist", "build"],
|
||||
"ts-node": {
|
||||
"transpileOnly": true,
|
||||
"swc": true
|
||||
}
|
||||
}
|
2
apps/api/types/index.ts
Normal file
2
apps/api/types/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./payload";
|
||||
export * from "./rich-text-export";
|
200
apps/api/types/payload.ts
Normal file
200
apps/api/types/payload.ts
Normal file
@ -0,0 +1,200 @@
|
||||
/* 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 interface Config {
|
||||
collections: {
|
||||
categories: Category;
|
||||
contents: Content;
|
||||
layouts: Layout;
|
||||
media: Media;
|
||||
pages: Page;
|
||||
tags: Tag;
|
||||
users: User;
|
||||
};
|
||||
globals: {};
|
||||
}
|
||||
export interface Category {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
export interface Content {
|
||||
id: string;
|
||||
name: string;
|
||||
slug?: string;
|
||||
description?: string;
|
||||
blocks?: (Menu | PageContent | PageList | SiteTitle)[];
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
export interface Menu {
|
||||
type: 'default';
|
||||
menus?: {
|
||||
mainMenu: MainMenu;
|
||||
id?: string;
|
||||
}[];
|
||||
id?: string;
|
||||
blockName?: string;
|
||||
blockType: 'menu';
|
||||
}
|
||||
export interface MainMenu {
|
||||
type?: 'reference' | 'custom' | 'none';
|
||||
newTab?: boolean;
|
||||
reference: {
|
||||
value: string | Page;
|
||||
relationTo: 'pages';
|
||||
};
|
||||
url: string;
|
||||
label: string;
|
||||
subMenu?: {
|
||||
link: Link;
|
||||
id?: string;
|
||||
}[];
|
||||
}
|
||||
export interface Page {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
author?: string | User;
|
||||
publishedDate?: string;
|
||||
categories?: string[] | Category[];
|
||||
tags?: string[] | Tag[];
|
||||
status?: 'Draft' | 'Published';
|
||||
layout?: string | Layout;
|
||||
content?: {
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
meta?: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
image?: string | Media;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
export interface User {
|
||||
id: string;
|
||||
name?: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
resetPasswordToken?: string;
|
||||
resetPasswordExpiration?: string;
|
||||
salt?: string;
|
||||
hash?: string;
|
||||
loginAttempts?: number;
|
||||
lockUntil?: string;
|
||||
password?: string;
|
||||
}
|
||||
export interface Tag {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
export interface Layout {
|
||||
id: string;
|
||||
name: string;
|
||||
slug?: string;
|
||||
description?: string;
|
||||
header?: {
|
||||
blocks?: (ReusableContent | Menu | SiteTitle)[];
|
||||
};
|
||||
body?: {
|
||||
blocks?: (ReusableContent | PageContent | PageList)[];
|
||||
};
|
||||
footer?: {
|
||||
blocks?: ReusableContent[];
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
export interface ReusableContent {
|
||||
reference?: {
|
||||
value: string | Content;
|
||||
relationTo: 'contents';
|
||||
};
|
||||
id?: string;
|
||||
blockName?: string;
|
||||
blockType: 'reusableContent';
|
||||
}
|
||||
export interface SiteTitle {
|
||||
siteName: string;
|
||||
id?: string;
|
||||
blockName?: string;
|
||||
blockType: 'siteTitle';
|
||||
}
|
||||
export interface PageContent {
|
||||
description?: string;
|
||||
id?: string;
|
||||
blockName?: string;
|
||||
blockType: 'pageContent';
|
||||
}
|
||||
export interface PageList {
|
||||
numberOfItems?: number;
|
||||
filterByCategories?:
|
||||
| {
|
||||
value: string;
|
||||
relationTo: 'categories';
|
||||
}[]
|
||||
| {
|
||||
value: Category;
|
||||
relationTo: 'categories';
|
||||
}[];
|
||||
filterByTags?:
|
||||
| {
|
||||
value: string;
|
||||
relationTo: 'tags';
|
||||
}[]
|
||||
| {
|
||||
value: Tag;
|
||||
relationTo: 'tags';
|
||||
}[];
|
||||
sortBy?: 'title' | 'createdAt' | 'updatedAt' | '-title' | '-createdAt' | '-updatedAt';
|
||||
id?: string;
|
||||
blockName?: string;
|
||||
blockType: 'pageList';
|
||||
}
|
||||
export interface Media {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string;
|
||||
filename?: string;
|
||||
mimeType?: string;
|
||||
filesize?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
sizes?: {
|
||||
thumbnail?: {
|
||||
url?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
mimeType?: string;
|
||||
filesize?: number;
|
||||
filename?: string;
|
||||
};
|
||||
sixteenByNineMedium?: {
|
||||
url?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
mimeType?: string;
|
||||
filesize?: number;
|
||||
filename?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
export interface Link {
|
||||
type?: 'reference' | 'custom';
|
||||
newTab?: boolean;
|
||||
reference: {
|
||||
value: string | Page;
|
||||
relationTo: 'pages';
|
||||
};
|
||||
url: string;
|
||||
label: string;
|
||||
}
|
22
apps/api/types/rich-text-export.ts
Normal file
22
apps/api/types/rich-text-export.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import type {
|
||||
RichTextElement,
|
||||
RichTextLeaf,
|
||||
} from "payload/dist/fields/config/types";
|
||||
import type { RichTextCustomElement, RichTextCustomLeaf } from "payload/types";
|
||||
|
||||
type DefaultRichTextLeaf = Exclude<RichTextLeaf, RichTextCustomLeaf>;
|
||||
export type FormattedText = {
|
||||
[key in DefaultRichTextLeaf]?: boolean;
|
||||
} & {
|
||||
text: string;
|
||||
};
|
||||
|
||||
type DefaultRichTextElement =
|
||||
| Exclude<RichTextElement, RichTextCustomElement>
|
||||
| "li"
|
||||
| "quote";
|
||||
export type FormattedElement = {
|
||||
type: DefaultRichTextElement;
|
||||
url?: string;
|
||||
children: FormattedText[];
|
||||
};
|
Reference in New Issue
Block a user