mirror of
https://github.com/supabase/supabase.git
synced 2026-10-11 04:15:04 +03:00
Merge branch 'master' of github.com:supabase/supabase into self-hosted/envoy-test
This commit is contained in:
commit
7d27b322c5
215 files changed
+3152
-64030
No files matched your search
@@ -8,8 +8,6 @@
|
||||
|
||||
/apps/studio/ @supabase/Dashboard
|
||||
|
||||
/apps/cms/ @supabase/marketing
|
||||
|
||||
/apps/www/ @supabase/marketing
|
||||
/apps/www/public/images/blog @supabase/marketing
|
||||
/apps/www/lib/redirects.js
|
||||
|
||||
@@ -24,10 +24,5 @@ apps/ui-library/public/r
|
||||
apps/**/.contentlayer
|
||||
# invalid JSON file
|
||||
packages/ui/src/components/Form/examples/PhoneProvidersSchema.json
|
||||
apps/cms/config/api.ts
|
||||
# files auto-generated by payload cms
|
||||
apps/cms/src/app/*
|
||||
apps/cms/src/migrations/*
|
||||
apps/cms/src/payload-types.ts
|
||||
# ignore because of <br/><br/>
|
||||
apps/www/_blog/2025-07-14-supabase-ui-platform-kit.mdx
|
||||
@@ -1,15 +0,0 @@
|
||||
# [LOCAL ENV] - NEVER USE PROD VALUES LOCALLY
|
||||
# as editing PayloadCMS typescript config INSTANTLY CHANGES DB SCHEMA in dev mode
|
||||
# The values set here are the default values provided when running `supabase start`.
|
||||
# Do not add secrets here, this file is git-tracked. Use .env.local instead.
|
||||
DATABASE_URI=postgresql://postgres:postgres@127.0.0.1:34322/postgres
|
||||
PAYLOAD_SECRET=secret
|
||||
|
||||
S3_BUCKET=cms
|
||||
S3_ACCESS_KEY_ID=625729a08b95bf1b7ff351a663f3a23c
|
||||
S3_SECRET_ACCESS_KEY=850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907
|
||||
S3_REGION=local
|
||||
S3_ENDPOINT=http://127.0.0.1:34321/storage/v1/s3
|
||||
|
||||
CRON_SECRET=secret
|
||||
PREVIEW_SECRET=secret
|
||||
@@ -1,42 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
/.idea/*
|
||||
!/.idea/runConfigurations
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
!.env
|
||||
.env.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
/media
|
||||
@@ -1,48 +0,0 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
**/node_modules
|
||||
|
||||
# Build artifacts
|
||||
.next/cache
|
||||
.turbo
|
||||
.eslintcache
|
||||
|
||||
# Development files
|
||||
*.log
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Test files
|
||||
**/*.test.*
|
||||
**/*.spec.*
|
||||
__tests__
|
||||
**/__tests__
|
||||
coverage
|
||||
|
||||
# Documentation
|
||||
README.md
|
||||
*.md
|
||||
docs
|
||||
|
||||
# IDE files
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Other apps (since we're only deploying CMS)
|
||||
../studio
|
||||
../www
|
||||
../docs
|
||||
../design-system
|
||||
../ui-library
|
||||
../../examples
|
||||
../../tests
|
||||
../../supabase
|
||||
../../packages/ui-patterns
|
||||
../../packages/ui
|
||||
../../packages/shared-data
|
||||
@@ -1,71 +0,0 @@
|
||||
# To use this Dockerfile, you have to set `output: 'standalone'` in your next.config.mjs file.
|
||||
# From https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile
|
||||
|
||||
FROM node:22.12.0-alpine AS base
|
||||
|
||||
# Install dependencies only when needed
|
||||
FROM base AS deps
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies based on the preferred package manager
|
||||
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
|
||||
RUN \
|
||||
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
|
||||
elif [ -f package-lock.json ]; then npm ci; \
|
||||
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
|
||||
else echo "Lockfile not found." && exit 1; \
|
||||
fi
|
||||
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
# Next.js collects completely anonymous telemetry data about general usage.
|
||||
# Learn more here: https://nextjs.org/telemetry
|
||||
# Uncomment the following line in case you want to disable telemetry during the build.
|
||||
# ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN \
|
||||
if [ -f yarn.lock ]; then yarn run build; \
|
||||
elif [ -f package-lock.json ]; then npm run build; \
|
||||
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
|
||||
else echo "Lockfile not found." && exit 1; \
|
||||
fi
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
# Uncomment the following line in case you want to disable telemetry during runtime.
|
||||
# ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
# Remove this line if you do not have this folder
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
# Set the correct permission for prerender cache
|
||||
RUN mkdir .next
|
||||
RUN chown nextjs:nodejs .next
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT 3000
|
||||
|
||||
# server.js is created by next build from the standalone output
|
||||
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
|
||||
CMD HOSTNAME="0.0.0.0" node server.js
|
||||
@@ -1,23 +0,0 @@
|
||||
# Payload CMS
|
||||
|
||||
### Local Development
|
||||
|
||||
1. run `cd apps/cms && supabase start` to start the local supabase project
|
||||
2. run `cp .env.example .env` to copy the example environment variables and update the variables. You'll need to add the `S3_` variables to your `.env` to use Supabase Storage
|
||||
3. `pnpm install && pnpm generate:importmap` to install dependencies and start the dev server
|
||||
4. run `pnpm dev` in the apps/cms folder or `pnpm dev:cms` from the root
|
||||
5. open `http://localhost:3030` to open the app in your browser
|
||||
|
||||
Follow the on-screen instructions to login and create the first admin user.
|
||||
|
||||
### Collections
|
||||
|
||||
Collections are what data looks like in the Payload cms schema. The following are the collections currently configured in the app.
|
||||
|
||||
- Authors
|
||||
- Categories
|
||||
- Events
|
||||
- Media
|
||||
- Posts
|
||||
- Tags
|
||||
- Users
|
||||
@@ -1,30 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
payload:
|
||||
image: node:18-alpine
|
||||
ports:
|
||||
- '3030:3030'
|
||||
volumes:
|
||||
- .:/home/node/app
|
||||
- node_modules:/home/node/app/node_modules
|
||||
working_dir: /home/node/app/
|
||||
command: sh -c "corepack enable && corepack prepare pnpm@latest --activate && pnpm install && pnpm dev"
|
||||
depends_on:
|
||||
- postgres
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
# Uncomment the following to use postgres
|
||||
postgres:
|
||||
restart: always
|
||||
image: postgres:latest
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
volumes:
|
||||
data:
|
||||
# pgdata:
|
||||
node_modules:
|
||||
@@ -1,4 +0,0 @@
|
||||
const { defineConfig } = require('eslint/config')
|
||||
const supabaseConfig = require('eslint-config-supabase/next')
|
||||
|
||||
module.exports = defineConfig([supabaseConfig])
|
||||
@@ -1,54 +0,0 @@
|
||||
import { withPayload } from '@payloadcms/next/withPayload'
|
||||
|
||||
const redirects = async () => {
|
||||
const internetExplorerRedirect = {
|
||||
destination: '/ie-incompatible.html',
|
||||
has: [
|
||||
{
|
||||
type: 'header',
|
||||
key: 'user-agent',
|
||||
value: '(.*Trident.*)', // all ie browsers
|
||||
},
|
||||
],
|
||||
permanent: false,
|
||||
source: '/:path((?!ie-incompatible.html$).*)', // all pages except the incompatibility page
|
||||
}
|
||||
|
||||
const redirects = [internetExplorerRedirect]
|
||||
|
||||
return redirects
|
||||
}
|
||||
|
||||
const WWW_SITE_ORIGIN =
|
||||
process.env.NEXT_PUBLIC_VERCEL_ENV === 'production'
|
||||
? 'https://supabase.com'
|
||||
: process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL &&
|
||||
typeof process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL === 'string'
|
||||
? `https://${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL.replace('cms-git-', 'zone-www-dot-com-git-')}`
|
||||
: 'http://localhost:3000'
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
images: {
|
||||
remotePatterns: [
|
||||
...[WWW_SITE_ORIGIN /* 'https://example.com' */].map((item) => {
|
||||
const url = new URL(item)
|
||||
|
||||
return {
|
||||
hostname: url.hostname,
|
||||
protocol: url.protocol?.replace(':', ''),
|
||||
}
|
||||
}),
|
||||
],
|
||||
},
|
||||
reactStrictMode: true,
|
||||
redirects,
|
||||
eslint: {
|
||||
// We are already running linting via GH action, this will skip linting during production build on Vercel
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
// Configure Sharp as an external package for server-side rendering
|
||||
serverExternalPackages: ['sharp'],
|
||||
}
|
||||
|
||||
export default withPayload(nextConfig, { devBundleServerPackages: false })
|
||||
@@ -1,64 +0,0 @@
|
||||
{
|
||||
"name": "cms",
|
||||
"version": "1.0.0",
|
||||
"description": "Payload CMS for Supabase",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_OPTIONS=--no-deprecation next build --turbopack",
|
||||
"ci": "pnpm migrate && pnpm build",
|
||||
"clean": "rimraf node_modules .next",
|
||||
"dev": "cross-env NODE_OPTIONS=--no-deprecation next dev --turbopack --port 3030",
|
||||
"dev:prod": "cross-env NODE_OPTIONS=--no-deprecation rm -rf .next && pnpm build && pnpm start",
|
||||
"devsafe": "rm -rf .next && cross-env NODE_OPTIONS=--no-deprecation next dev",
|
||||
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
|
||||
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
|
||||
"lint": "eslint .",
|
||||
"migrate": "cross-env NODE_OPTIONS=--no-deprecation tsx scripts/migrate.ts",
|
||||
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
|
||||
"start": "cross-env NODE_OPTIONS=--no-deprecation next start",
|
||||
"vercel-build": "pnpm migrate && pnpm build",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/admin-bar": "^3.52.0",
|
||||
"@payloadcms/db-postgres": "^3.73.0",
|
||||
"@payloadcms/live-preview-react": "^3.52.0",
|
||||
"@payloadcms/next": "3.52.0",
|
||||
"@payloadcms/payload-cloud": "3.60.0",
|
||||
"@payloadcms/plugin-form-builder": "3.52.0",
|
||||
"@payloadcms/plugin-nested-docs": "3.52.0",
|
||||
"@payloadcms/plugin-seo": "3.52.0",
|
||||
"@payloadcms/richtext-lexical": "3.52.0",
|
||||
"@payloadcms/storage-s3": "3.52.0",
|
||||
"@payloadcms/ui": "3.52.0",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"common": "workspace:*",
|
||||
"config": "workspace:*",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint-config-supabase": "workspace:*",
|
||||
"graphql": "^16.11.0",
|
||||
"image-size": "2.0.2",
|
||||
"lucide-react": "^0.511.0",
|
||||
"next": "catalog:",
|
||||
"payload": "3.52.0",
|
||||
"pg": "^8.16.3",
|
||||
"prism-react-renderer": "^2.3.1",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"sharp": "~0.34.5",
|
||||
"tailwind-merge": "^1.13.2",
|
||||
"tsx": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"externals": {
|
||||
"sharp": "commonjs sharp"
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import payload from 'payload'
|
||||
|
||||
// Use a minimal config for migrations to avoid importing the full app graph during build
|
||||
import payloadConfig from '../src/payload.migrate.config.ts'
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
await payload.init({ config: payloadConfig })
|
||||
|
||||
// Ensure non-interactive: remove any dev-mode migration sentinel rows
|
||||
try {
|
||||
await payload.delete({
|
||||
collection: 'payload-migrations',
|
||||
where: { batch: { equals: -1 } },
|
||||
})
|
||||
} catch {}
|
||||
|
||||
await payload.db.migrate()
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('✅ Payload migrations complete')
|
||||
process.exit(0)
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('❌ Payload migrations failed:', err)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { AccessArgs, FieldAccess } from 'payload'
|
||||
|
||||
import type { User } from '../payload-types'
|
||||
|
||||
type isAdmin = (args: AccessArgs<User>) => boolean
|
||||
|
||||
export const isAdmin: isAdmin = ({ req: { user } }) => {
|
||||
// Return true or false based on if the user has an admin role
|
||||
return Boolean(user?.roles?.includes('admin'))
|
||||
}
|
||||
|
||||
export const isAdminFieldLevel: FieldAccess<{ id: string }, User> = ({ req: { user } }) => {
|
||||
// Return true or false based on if the user has an admin role
|
||||
return Boolean(user?.roles?.includes('admin'))
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { Access } from 'payload'
|
||||
|
||||
export const isAdminOrSelf: Access = ({ req: { user } }) => {
|
||||
// Need to be logged in
|
||||
if (user) {
|
||||
// If user has role of 'admin'
|
||||
if (user.roles?.includes('admin')) {
|
||||
return true
|
||||
}
|
||||
|
||||
// If any other type of user, only provide access to themselves
|
||||
return {
|
||||
id: {
|
||||
equals: user.id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Reject everyone else
|
||||
return false
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import type { Access } from 'payload'
|
||||
|
||||
export const isAnyone: Access = () => true
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { AccessArgs } from 'payload'
|
||||
|
||||
import type { User } from '../payload-types'
|
||||
|
||||
type isAuthenticated = (args: AccessArgs<User>) => boolean
|
||||
|
||||
export const isAuthenticated: isAuthenticated = ({ req: { user } }) => {
|
||||
return Boolean(user)
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import React from 'react'
|
||||
import './styles.css'
|
||||
|
||||
export const metadata = {
|
||||
description: 'Content Management System for the Supabase website',
|
||||
title: 'Supabase CMS',
|
||||
}
|
||||
|
||||
export default async function RootLayout(props: { children: React.ReactNode }) {
|
||||
const { children } = props
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<main>{children}</main>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export default async function HomePage() {
|
||||
redirect('/admin')
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
:root {
|
||||
--font-mono: 'Roboto Mono', monospace;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 18px;
|
||||
line-height: 32px;
|
||||
|
||||
background: rgb(0, 0, 0);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: system-ui;
|
||||
font-size: 18px;
|
||||
line-height: 32px;
|
||||
|
||||
margin: 0;
|
||||
color: rgb(1000, 1000, 1000);
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 15px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 40px 0;
|
||||
font-size: 64px;
|
||||
line-height: 70px;
|
||||
font-weight: bold;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
margin: 24px 0;
|
||||
font-size: 42px;
|
||||
line-height: 42px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 38px;
|
||||
line-height: 38px;
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
font-size: 32px;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 24px 0;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
margin: calc(var(--base) * 0.75) 0;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: currentColor;
|
||||
|
||||
&:focus {
|
||||
opacity: 0.8;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.home {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
padding: 45px;
|
||||
max-width: 1024px;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
|
||||
@media (max-width: 400px) {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.admin {
|
||||
color: rgb(0, 0, 0);
|
||||
background: rgb(1000, 1000, 1000);
|
||||
border: 1px solid rgb(0, 0, 0);
|
||||
}
|
||||
|
||||
.docs {
|
||||
color: rgb(1000, 1000, 1000);
|
||||
background: rgb(0, 0, 0);
|
||||
border: 1px solid rgb(1000, 1000, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.codeLink {
|
||||
text-decoration: none;
|
||||
padding: 0 0.5rem;
|
||||
background: rgb(60, 60, 60);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||
import { importMap } from '../importMap'
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
segments: string[]
|
||||
}>
|
||||
searchParams: Promise<{
|
||||
[key: string]: string | string[]
|
||||
}>
|
||||
}
|
||||
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams })
|
||||
|
||||
const NotFound = ({ params, searchParams }: Args) =>
|
||||
NotFoundPage({ config, params, searchParams, importMap })
|
||||
|
||||
export default NotFound
|
||||
@@ -1,24 +0,0 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||
import { importMap } from '../importMap'
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
segments: string[]
|
||||
}>
|
||||
searchParams: Promise<{
|
||||
[key: string]: string | string[]
|
||||
}>
|
||||
}
|
||||
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams })
|
||||
|
||||
const Page = ({ params, searchParams }: Args) =>
|
||||
RootPage({ config, params, searchParams, importMap })
|
||||
|
||||
export default Page
|
||||
@@ -1,43 +0,0 @@
|
||||
import { SlugComponent as SlugComponent_92cc057d0a2abb4f6cf0307edf59f986 } from '@/fields/slug/SlugComponent'
|
||||
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { FixedToolbarFeatureClient as FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { OverviewComponent as OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
|
||||
import { MetaTitleComponent as MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
|
||||
import { MetaImageComponent as MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
|
||||
import { MetaDescriptionComponent as MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
|
||||
import { PreviewComponent as PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
|
||||
import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client'
|
||||
|
||||
export const importMap = {
|
||||
"@/fields/slug/SlugComponent#SlugComponent": SlugComponent_92cc057d0a2abb4f6cf0307edf59f986,
|
||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient": FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/plugin-seo/client#OverviewComponent": OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||
"@payloadcms/plugin-seo/client#MetaTitleComponent": MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||
"@payloadcms/plugin-seo/client#MetaImageComponent": MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||
"@payloadcms/plugin-seo/client#MetaDescriptionComponent": MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||
"@payloadcms/plugin-seo/client#PreviewComponent": PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||
"@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import '@payloadcms/next/css'
|
||||
import {
|
||||
REST_DELETE,
|
||||
REST_GET,
|
||||
REST_OPTIONS,
|
||||
REST_PATCH,
|
||||
REST_POST,
|
||||
REST_PUT,
|
||||
} from '@payloadcms/next/routes'
|
||||
|
||||
export const GET = REST_GET(config)
|
||||
export const POST = REST_POST(config)
|
||||
export const DELETE = REST_DELETE(config)
|
||||
export const PATCH = REST_PATCH(config)
|
||||
export const PUT = REST_PUT(config)
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
Whitespace-only changes.
@@ -1,31 +0,0 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import '@payloadcms/next/css'
|
||||
import type { ServerFunctionClient } from 'payload'
|
||||
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
|
||||
import React from 'react'
|
||||
|
||||
import { importMap } from './admin/importMap.js'
|
||||
import './custom.scss'
|
||||
|
||||
type Args = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const serverFunction: ServerFunctionClient = async function (args) {
|
||||
'use server'
|
||||
return handleServerFunctions({
|
||||
...args,
|
||||
config,
|
||||
importMap,
|
||||
})
|
||||
}
|
||||
|
||||
const Layout = ({ children }: Args) => (
|
||||
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
|
||||
{children}
|
||||
</RootLayout>
|
||||
)
|
||||
|
||||
export default Layout
|
||||
@@ -1,14 +0,0 @@
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
export const GET = async () => {
|
||||
const payload = await getPayload({
|
||||
config: configPromise,
|
||||
})
|
||||
|
||||
const data = await payload.find({
|
||||
collection: 'users',
|
||||
})
|
||||
|
||||
return Response.json(data)
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import RichText from '@/components/RichText'
|
||||
import type { BannerBlock as BannerBlockProps } from '@/payload-types'
|
||||
import { cn } from '@/utilities/ui'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
} & BannerBlockProps
|
||||
|
||||
export const BannerBlock: React.FC<Props> = ({ className, content, style }) => {
|
||||
return (
|
||||
<div className={cn('mx-auto my-8 w-full', className)}>
|
||||
<div
|
||||
className={cn('border py-3 px-6 flex items-center rounded', {
|
||||
'border-border bg-card': style === 'info',
|
||||
'border-error bg-error/30': style === 'error',
|
||||
'border-success bg-success/30': style === 'success',
|
||||
'border-warning bg-warning/30': style === 'warning',
|
||||
})}
|
||||
>
|
||||
<RichText data={content} enableGutter={false} enableProse={false} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import {
|
||||
FixedToolbarFeature,
|
||||
InlineToolbarFeature,
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import type { Block } from 'payload'
|
||||
|
||||
export const Banner: Block = {
|
||||
slug: 'banner',
|
||||
fields: [
|
||||
{
|
||||
name: 'style',
|
||||
type: 'select',
|
||||
defaultValue: 'info',
|
||||
options: [
|
||||
{ label: 'Info', value: 'info' },
|
||||
{ label: 'Warning', value: 'warning' },
|
||||
{ label: 'Error', value: 'error' },
|
||||
{ label: 'Success', value: 'success' },
|
||||
],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ rootFeatures }) => {
|
||||
return [...rootFeatures, FixedToolbarFeature(), InlineToolbarFeature()]
|
||||
},
|
||||
}),
|
||||
label: false,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
interfaceName: 'BannerBlock',
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { Highlight, themes } from 'prism-react-renderer'
|
||||
import React from 'react'
|
||||
|
||||
import { CopyButton } from './CopyButton'
|
||||
|
||||
type Props = {
|
||||
code: string
|
||||
language?: string
|
||||
}
|
||||
|
||||
export const Code: React.FC<Props> = ({ code, language = '' }) => {
|
||||
if (!code) return null
|
||||
|
||||
return (
|
||||
<Highlight code={code} language={language} theme={themes.vsDark}>
|
||||
{({ getLineProps, getTokenProps, tokens }) => (
|
||||
<pre className="bg-black p-4 border text-xs border-border rounded overflow-x-auto">
|
||||
{tokens.map((line, i) => (
|
||||
<div key={i} {...getLineProps({ className: 'table-row', line })}>
|
||||
<span className="table-cell select-none text-right text-white/25">{i + 1}</span>
|
||||
<span className="table-cell pl-4">
|
||||
{line.map((token, key) => (
|
||||
<span key={key} {...getTokenProps({ token })} />
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<CopyButton code={code} />
|
||||
</pre>
|
||||
)}
|
||||
</Highlight>
|
||||
)
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import { Code } from './Component.client'
|
||||
|
||||
export type CodeBlockProps = {
|
||||
code: string
|
||||
language?: string
|
||||
blockType: 'code'
|
||||
}
|
||||
|
||||
type Props = CodeBlockProps & {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const CodeBlock: React.FC<Props> = ({ className, code, language }) => {
|
||||
return (
|
||||
<div className={[className, 'not-prose'].filter(Boolean).join(' ')}>
|
||||
<Code code={code} language={language} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { CopyIcon } from '@payloadcms/ui/icons/Copy'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
export function CopyButton({ code }: { code: string }) {
|
||||
const [text, setText] = useState('Copy')
|
||||
|
||||
function updateCopyStatus() {
|
||||
if (text === 'Copy') {
|
||||
setText(() => 'Copied!')
|
||||
setTimeout(() => {
|
||||
setText(() => 'Copy')
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-end align-middle">
|
||||
<Button
|
||||
className="flex gap-1"
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(code)
|
||||
updateCopyStatus()
|
||||
}}
|
||||
>
|
||||
<p>{text}</p>
|
||||
<CopyIcon />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import type { Block } from 'payload'
|
||||
|
||||
export const Code: Block = {
|
||||
slug: 'code',
|
||||
interfaceName: 'CodeBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'language',
|
||||
type: 'select',
|
||||
defaultValue: 'typescript',
|
||||
options: [
|
||||
{
|
||||
label: 'SQL',
|
||||
value: 'sql',
|
||||
},
|
||||
{
|
||||
label: 'JSON',
|
||||
value: 'json',
|
||||
},
|
||||
{
|
||||
label: 'bash',
|
||||
value: 'bash',
|
||||
},
|
||||
{
|
||||
label: 'Javascript',
|
||||
value: 'js',
|
||||
},
|
||||
{
|
||||
label: 'Typescript',
|
||||
value: 'ts',
|
||||
},
|
||||
{
|
||||
label: 'tsx',
|
||||
value: 'tsx',
|
||||
},
|
||||
{
|
||||
label: 'Python',
|
||||
value: 'py',
|
||||
},
|
||||
{
|
||||
label: 'kotlin',
|
||||
value: 'kotlin',
|
||||
},
|
||||
{
|
||||
label: 'yaml',
|
||||
value: 'yaml',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'code',
|
||||
type: 'code',
|
||||
label: false,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import type { StaticImageData } from 'next/image'
|
||||
import React from 'react'
|
||||
|
||||
import { Media } from '../../components/Media'
|
||||
import RichText from '@/components/RichText'
|
||||
import type { MediaBlock as MediaBlockProps } from '@/payload-types'
|
||||
import { cn } from '@/utilities/ui'
|
||||
|
||||
type Props = MediaBlockProps & {
|
||||
breakout?: boolean
|
||||
captionClassName?: string
|
||||
className?: string
|
||||
enableGutter?: boolean
|
||||
imgClassName?: string
|
||||
staticImage?: StaticImageData
|
||||
disableInnerContainer?: boolean
|
||||
}
|
||||
|
||||
export const MediaBlock: React.FC<Props> = (props) => {
|
||||
const {
|
||||
captionClassName,
|
||||
className,
|
||||
enableGutter = true,
|
||||
imgClassName,
|
||||
media,
|
||||
staticImage,
|
||||
disableInnerContainer,
|
||||
} = props
|
||||
|
||||
let caption
|
||||
if (media && typeof media === 'object') caption = media.caption
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'',
|
||||
{
|
||||
container: enableGutter,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{(media || staticImage) && (
|
||||
<Media
|
||||
imgClassName={cn('border border-border rounded-[0.8rem]', imgClassName)}
|
||||
resource={media}
|
||||
src={staticImage}
|
||||
/>
|
||||
)}
|
||||
{caption && (
|
||||
<div
|
||||
className={cn(
|
||||
'mt-6',
|
||||
{
|
||||
container: !disableInnerContainer,
|
||||
},
|
||||
captionClassName
|
||||
)}
|
||||
>
|
||||
<RichText data={caption} enableGutter={false} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { Block } from 'payload'
|
||||
|
||||
export const MediaBlock: Block = {
|
||||
slug: 'mediaBlock',
|
||||
interfaceName: 'MediaBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'media',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import type { QuoteBlock as QuoteBlockProps } from '@/payload-types'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
} & QuoteBlockProps
|
||||
|
||||
export const QuoteBlock: React.FC<Props> = ({ className, img, caption, text }) => {
|
||||
return `<Quote img={${img}} caption={${caption ?? ''}} className={${className}}>
|
||||
|
||||
{${text}}
|
||||
|
||||
</Quote>
|
||||
`
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { Block } from 'payload'
|
||||
|
||||
export const Quote: Block = {
|
||||
slug: 'quote',
|
||||
fields: [
|
||||
{
|
||||
name: 'img',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
label: 'Avatar',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'caption',
|
||||
type: 'text',
|
||||
label: 'Caption',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'text',
|
||||
type: 'textarea',
|
||||
label: 'Quote Text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
interfaceName: 'QuoteBlock',
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import type { YouTubeBlock as YouTubeBlockProps } from '@/payload-types'
|
||||
import { cn } from '@/utilities/ui'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
} & YouTubeBlockProps
|
||||
|
||||
export const YouTubeBlock: React.FC<Props> = ({ className, youtubeId }) => {
|
||||
return (
|
||||
<div className={cn('video-container', className)}>
|
||||
<iframe
|
||||
className="w-full"
|
||||
src={`https://www.youtube-nocookie.com/embed/${youtubeId}`}
|
||||
title="YouTube video"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { Block } from 'payload'
|
||||
|
||||
export const YouTube: Block = {
|
||||
slug: 'youtube',
|
||||
fields: [
|
||||
{
|
||||
name: 'youtubeId',
|
||||
type: 'text',
|
||||
label: 'YouTube Video ID',
|
||||
required: true,
|
||||
admin: {
|
||||
description:
|
||||
'Enter the YouTube video ID (e.g., "dQw4w9WgXcQ" from https://www.youtube.com/watch?v=dQw4w9WgXcQ)',
|
||||
},
|
||||
},
|
||||
],
|
||||
interfaceName: 'YouTubeBlock',
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Authors: CollectionConfig = {
|
||||
slug: 'authors',
|
||||
admin: {
|
||||
useAsTitle: 'author',
|
||||
defaultColumns: ['author', 'position', 'author_id'],
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'author',
|
||||
type: 'text',
|
||||
required: true,
|
||||
label: 'Author Name',
|
||||
index: true,
|
||||
},
|
||||
{
|
||||
name: 'author_id',
|
||||
type: 'text',
|
||||
required: true,
|
||||
unique: true,
|
||||
label: 'Author ID',
|
||||
admin: {
|
||||
description: 'Unique identifier for the author',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'username',
|
||||
type: 'text',
|
||||
label: 'Username',
|
||||
admin: {
|
||||
description: 'GitHub/social username',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'position',
|
||||
type: 'text',
|
||||
label: 'Position',
|
||||
},
|
||||
{
|
||||
name: 'company',
|
||||
type: 'text',
|
||||
label: 'Company',
|
||||
admin: {
|
||||
description: 'Company name (for external/guest authors)',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'author_url',
|
||||
type: 'text',
|
||||
label: 'Profile URL',
|
||||
admin: {
|
||||
description: 'Link to GitHub, Twitter, LinkedIn, etc.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'author_image_url',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
required: false,
|
||||
label: 'Profile Image',
|
||||
},
|
||||
],
|
||||
timestamps: true,
|
||||
}
|
||||
|
||||
export default Authors
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { isAdmin } from '../access/isAdmin'
|
||||
|
||||
export const Categories: CollectionConfig = {
|
||||
slug: 'categories',
|
||||
admin: {
|
||||
useAsTitle: 'name',
|
||||
},
|
||||
access: {
|
||||
create: isAdmin,
|
||||
read: () => true,
|
||||
update: isAdmin,
|
||||
delete: isAdmin,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
],
|
||||
timestamps: true,
|
||||
}
|
||||
|
||||
export default Categories
|
||||
@@ -1,56 +0,0 @@
|
||||
import type { CollectionAfterChangeHook, CollectionAfterDeleteHook } from 'payload'
|
||||
|
||||
// Avoid importing next/cache at module scope so migrations can run in a plain Node env
|
||||
|
||||
import type { Customer } from '../../../payload-types'
|
||||
|
||||
export const revalidateCustomer: CollectionAfterChangeHook<Customer> = async ({
|
||||
doc,
|
||||
previousDoc,
|
||||
req: { payload, context },
|
||||
}) => {
|
||||
if (!context.disableRevalidate) {
|
||||
if (doc._status === 'published') {
|
||||
const path = `/customers/${doc.slug}`
|
||||
|
||||
payload.logger.info(`Revalidating event at path: ${path}`)
|
||||
try {
|
||||
const { revalidatePath, revalidateTag } = await import('next/cache')
|
||||
revalidatePath(path)
|
||||
revalidateTag('customers-sitemap')
|
||||
} catch {
|
||||
// no-op when not running inside Next runtime (e.g., during payload migrate)
|
||||
}
|
||||
}
|
||||
|
||||
// If the event was previously published, we need to revalidate the old path
|
||||
if (previousDoc._status === 'published' && doc._status !== 'published') {
|
||||
const oldPath = `/customers/${previousDoc.slug}`
|
||||
|
||||
payload.logger.info(`Revalidating old event at path: ${oldPath}`)
|
||||
|
||||
try {
|
||||
const { revalidatePath, revalidateTag } = await import('next/cache')
|
||||
revalidatePath(oldPath)
|
||||
revalidateTag('customers-sitemap')
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
export const revalidateDelete: CollectionAfterDeleteHook<Customer> = async ({
|
||||
doc,
|
||||
req: { context },
|
||||
}) => {
|
||||
if (!context.disableRevalidate) {
|
||||
const path = `/customers/${doc?.slug}`
|
||||
try {
|
||||
const { revalidatePath, revalidateTag } = await import('next/cache')
|
||||
revalidatePath(path)
|
||||
revalidateTag('customers-sitemap')
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return doc
|
||||
}
|
||||
@@ -1,317 +0,0 @@
|
||||
import {
|
||||
MetaDescriptionField,
|
||||
MetaImageField,
|
||||
MetaTitleField,
|
||||
OverviewField,
|
||||
PreviewField,
|
||||
} from '@payloadcms/plugin-seo/fields'
|
||||
import {
|
||||
BlocksFeature,
|
||||
FixedToolbarFeature,
|
||||
HeadingFeature,
|
||||
HorizontalRuleFeature,
|
||||
InlineToolbarFeature,
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { isAnyone } from '../../access/isAnyone.ts'
|
||||
import { isAuthenticated } from '../../access/isAuthenticated.ts'
|
||||
import { Banner } from '../../blocks/Banner/config.ts'
|
||||
import { Code } from '../../blocks/Code/config.ts'
|
||||
import { MediaBlock } from '../../blocks/MediaBlock/config.ts'
|
||||
import { Quote } from '../../blocks/Quote/config.ts'
|
||||
import { YouTube } from '../../blocks/YouTube/config.ts'
|
||||
import { slugField } from '../../fields/slug/index.ts'
|
||||
import { WWW_SITE_ORIGIN } from '../../utilities/constants.ts'
|
||||
import { revalidateCustomer, revalidateDelete } from './hooks/revalidateCustomer.ts'
|
||||
|
||||
const industryOptions = [
|
||||
{ label: 'Healthcare', value: 'healthcare' },
|
||||
{ label: 'Fintech', value: 'fintech' },
|
||||
{ label: 'E-commerce', value: 'ecommerce' },
|
||||
{ label: 'Education', value: 'education' },
|
||||
{ label: 'Gaming', value: 'gaming' },
|
||||
{ label: 'Media', value: 'media' },
|
||||
{ label: 'Real Estate', value: 'real-estate' },
|
||||
{ label: 'SaaS', value: 'saas' },
|
||||
{ label: 'Social', value: 'social' },
|
||||
{ label: 'Analytics', value: 'analytics' },
|
||||
{ label: 'AI', value: 'ai' },
|
||||
{ label: 'Developer Tools', value: 'developer-tools' },
|
||||
]
|
||||
|
||||
const companySizeOptions = [
|
||||
{ label: 'Startup', value: 'startup' },
|
||||
{ label: 'Enterprise', value: 'enterprise' },
|
||||
{ label: 'Independent Developer', value: 'indie_dev' },
|
||||
]
|
||||
|
||||
const regionOptions = [
|
||||
{ label: 'Asia', value: 'Asia' },
|
||||
{ label: 'Europe', value: 'Europe' },
|
||||
{ label: 'North America', value: 'North America' },
|
||||
{ label: 'South America', value: 'South America' },
|
||||
{ label: 'Africa', value: 'Africa' },
|
||||
{ label: 'Oceania', value: 'Oceania' },
|
||||
]
|
||||
|
||||
const supabaseProductOptions = [
|
||||
{ label: 'Database', value: 'database' },
|
||||
{ label: 'Auth', value: 'auth' },
|
||||
{ label: 'Storage', value: 'storage' },
|
||||
{ label: 'Realtime', value: 'realtime' },
|
||||
{ label: 'Functions', value: 'functions' },
|
||||
{ label: 'Vector', value: 'vector' },
|
||||
]
|
||||
|
||||
export const Customers: CollectionConfig = {
|
||||
slug: 'customers',
|
||||
admin: {
|
||||
useAsTitle: 'name',
|
||||
defaultColumns: ['name', 'slug', 'updatedAt'],
|
||||
preview: (data) => {
|
||||
const baseUrl = WWW_SITE_ORIGIN || 'http://localhost:3000'
|
||||
const isDraft = data?._status === 'draft'
|
||||
return `${baseUrl}/customers/${data?.slug}${isDraft ? '?preview=true' : ''}`
|
||||
},
|
||||
},
|
||||
access: {
|
||||
// create: isAuthenticated,
|
||||
// delete: isAuthenticated,
|
||||
// read: isAnyone,
|
||||
// update: isAuthenticated,
|
||||
create: () => false,
|
||||
delete: () => false,
|
||||
read: () => false,
|
||||
update: () => false,
|
||||
},
|
||||
defaultPopulate: {
|
||||
name: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
categories: true,
|
||||
meta: {
|
||||
image: true,
|
||||
description: true,
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: false,
|
||||
},
|
||||
...slugField('name'),
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
label: 'Content',
|
||||
fields: [
|
||||
{
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ rootFeatures }) => {
|
||||
return [
|
||||
...rootFeatures,
|
||||
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
|
||||
BlocksFeature({ blocks: [Banner, Code, MediaBlock, Quote, YouTube] }),
|
||||
FixedToolbarFeature(),
|
||||
InlineToolbarFeature(),
|
||||
HorizontalRuleFeature(),
|
||||
]
|
||||
},
|
||||
}),
|
||||
label: false,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Metadata',
|
||||
fields: [
|
||||
{
|
||||
name: 'description',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'about',
|
||||
type: 'textarea',
|
||||
admin: {
|
||||
description: 'Short description about the company',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'company_url',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: 'URL of the company website',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'stats',
|
||||
type: 'array',
|
||||
admin: {
|
||||
description: 'Key statistics or metrics to highlight',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'stat',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'label',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'misc',
|
||||
type: 'array',
|
||||
admin: {
|
||||
description: 'Miscellaneous information (e.g., Founded, Location)',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'label',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'industry',
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
options: industryOptions,
|
||||
admin: {
|
||||
description: 'Industry categories',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'company_size',
|
||||
type: 'select',
|
||||
options: companySizeOptions,
|
||||
admin: {
|
||||
description: 'Size of the company',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'region',
|
||||
type: 'select',
|
||||
options: regionOptions,
|
||||
admin: {
|
||||
description: 'Geographic region',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'supabase_products',
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
options: supabaseProductOptions,
|
||||
admin: {
|
||||
description: 'Supabase products being used',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'meta',
|
||||
label: 'SEO',
|
||||
fields: [
|
||||
OverviewField({
|
||||
titlePath: 'meta.name',
|
||||
descriptionPath: 'meta.description',
|
||||
imagePath: 'meta.image',
|
||||
}),
|
||||
MetaTitleField({
|
||||
hasGenerateFn: true,
|
||||
}),
|
||||
MetaImageField({
|
||||
relationTo: 'media',
|
||||
}),
|
||||
|
||||
MetaDescriptionField({}),
|
||||
PreviewField({
|
||||
// if the `generateUrl` function is configured
|
||||
hasGenerateFn: true,
|
||||
|
||||
// field paths to match the target field for data
|
||||
titlePath: 'meta.name',
|
||||
descriptionPath: 'meta.description',
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'publishedAt',
|
||||
type: 'date',
|
||||
admin: {
|
||||
date: {
|
||||
pickerAppearance: 'dayAndTime',
|
||||
},
|
||||
position: 'sidebar',
|
||||
},
|
||||
hooks: {
|
||||
beforeChange: [
|
||||
({ siblingData, value }) => {
|
||||
if (siblingData._status === 'published' && !value) {
|
||||
return new Date()
|
||||
}
|
||||
return value
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'logo',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
required: false,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'logo_inverse',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
required: false,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
description: 'Light mode logo',
|
||||
},
|
||||
},
|
||||
],
|
||||
timestamps: true,
|
||||
hooks: {
|
||||
afterChange: [revalidateCustomer],
|
||||
afterDelete: [revalidateDelete],
|
||||
},
|
||||
versions: {
|
||||
drafts: {
|
||||
// autosave: {
|
||||
// interval: 100,
|
||||
// },
|
||||
schedulePublish: true,
|
||||
},
|
||||
maxPerDoc: 50,
|
||||
},
|
||||
}
|
||||
|
||||
export default Customers
|
||||
@@ -1,54 +0,0 @@
|
||||
import type { CollectionAfterChangeHook, CollectionAfterDeleteHook } from 'payload'
|
||||
|
||||
// Avoid importing next/cache at module scope so migrations can run in a plain Node env
|
||||
|
||||
import type { Event } from '../../../payload-types'
|
||||
|
||||
export const revalidateEvent: CollectionAfterChangeHook<Event> = async ({
|
||||
doc,
|
||||
previousDoc,
|
||||
req: { payload, context },
|
||||
}) => {
|
||||
if (!context.disableRevalidate) {
|
||||
if (doc._status === 'published') {
|
||||
const path = `/events/${doc.slug}`
|
||||
|
||||
payload.logger.info(`Revalidating event at path: ${path}`)
|
||||
try {
|
||||
const { revalidatePath, revalidateTag } = await import('next/cache')
|
||||
revalidatePath(path)
|
||||
revalidateTag('events-sitemap')
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// If the event was previously published, we need to revalidate the old path
|
||||
if (previousDoc._status === 'published' && doc._status !== 'published') {
|
||||
const oldPath = `/events/${previousDoc.slug}`
|
||||
|
||||
payload.logger.info(`Revalidating old event at path: ${oldPath}`)
|
||||
|
||||
try {
|
||||
const { revalidatePath, revalidateTag } = await import('next/cache')
|
||||
revalidatePath(oldPath)
|
||||
revalidateTag('events-sitemap')
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
export const revalidateDelete: CollectionAfterDeleteHook<Event> = async ({
|
||||
doc,
|
||||
req: { context },
|
||||
}) => {
|
||||
if (!context.disableRevalidate) {
|
||||
const path = `/events/${doc?.slug}`
|
||||
try {
|
||||
const { revalidatePath, revalidateTag } = await import('next/cache')
|
||||
revalidatePath(path)
|
||||
revalidateTag('events-sitemap')
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return doc
|
||||
}
|
||||
@@ -1,407 +0,0 @@
|
||||
import {
|
||||
MetaDescriptionField,
|
||||
MetaImageField,
|
||||
MetaTitleField,
|
||||
OverviewField,
|
||||
PreviewField,
|
||||
} from '@payloadcms/plugin-seo/fields'
|
||||
import {
|
||||
BlocksFeature,
|
||||
FixedToolbarFeature,
|
||||
HeadingFeature,
|
||||
HorizontalRuleFeature,
|
||||
InlineToolbarFeature,
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { isAnyone } from '../../access/isAnyone.ts'
|
||||
import { isAuthenticated } from '../../access/isAuthenticated.ts'
|
||||
import { Banner } from '../../blocks/Banner/config.ts'
|
||||
import { Code } from '../../blocks/Code/config.ts'
|
||||
import { MediaBlock } from '../../blocks/MediaBlock/config.ts'
|
||||
import { Quote } from '../../blocks/Quote/config.ts'
|
||||
import { YouTube } from '../../blocks/YouTube/config.ts'
|
||||
import { slugField } from '../../fields/slug/index.ts'
|
||||
import { WWW_SITE_ORIGIN } from '../../utilities/constants.ts'
|
||||
import { timezoneOptions } from '../../utilities/timezones.ts'
|
||||
import { revalidateDelete, revalidateEvent } from './hooks/revalidateEvent.ts'
|
||||
|
||||
const eventTypeOptions = [
|
||||
{ label: 'Conference', value: 'conference' },
|
||||
{ label: 'Hackathon', value: 'hackathon' },
|
||||
{ label: 'Launch Week', value: 'launch-week' },
|
||||
{ label: 'Meetup', value: 'meetup' },
|
||||
{ label: 'Office Hours', value: 'office-hours' },
|
||||
{ label: 'Talk', value: 'talk' },
|
||||
{ label: 'Webinar', value: 'webinar' },
|
||||
{ label: 'Workshop', value: 'workshop' },
|
||||
{ label: 'Other', value: 'other' },
|
||||
]
|
||||
|
||||
export const Events: CollectionConfig = {
|
||||
slug: 'events',
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
defaultColumns: ['title', 'slug', 'updatedAt'],
|
||||
preview: (data) => {
|
||||
const baseUrl = WWW_SITE_ORIGIN || 'http://localhost:3000'
|
||||
// Always use the preview route to ensure draft mode is enabled
|
||||
return `${baseUrl}/api-v2/cms/preview?slug=${data?.slug}&path=events&secret=${process.env.PREVIEW_SECRET || 'secret'}`
|
||||
},
|
||||
},
|
||||
access: {
|
||||
// create: isAuthenticated,
|
||||
// delete: isAuthenticated,
|
||||
// read: isAnyone,
|
||||
// update: isAuthenticated,
|
||||
create: () => false,
|
||||
delete: () => false,
|
||||
read: () => false,
|
||||
update: () => false,
|
||||
},
|
||||
defaultPopulate: {
|
||||
title: true,
|
||||
slug: true,
|
||||
categories: true,
|
||||
meta: {
|
||||
image: true,
|
||||
description: true,
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
...slugField(),
|
||||
{
|
||||
name: 'subtitle',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: 'Used in the event page as subtitle.',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
label: 'Content',
|
||||
fields: [
|
||||
{
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ rootFeatures }) => {
|
||||
return [
|
||||
...rootFeatures,
|
||||
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
|
||||
BlocksFeature({ blocks: [Banner, Code, MediaBlock, Quote, YouTube] }),
|
||||
FixedToolbarFeature(),
|
||||
InlineToolbarFeature(),
|
||||
HorizontalRuleFeature(),
|
||||
]
|
||||
},
|
||||
}),
|
||||
label: false,
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Metadata',
|
||||
fields: [
|
||||
{
|
||||
name: 'thumb',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'image',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
options: eventTypeOptions,
|
||||
admin: {
|
||||
description: 'Event type',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
type: 'date',
|
||||
admin: {
|
||||
date: {
|
||||
pickerAppearance: 'dayAndTime',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'timezone',
|
||||
type: 'select',
|
||||
options: timezoneOptions,
|
||||
},
|
||||
{
|
||||
name: 'showEndDate',
|
||||
type: 'checkbox',
|
||||
defaultValue: false,
|
||||
admin: {},
|
||||
},
|
||||
{
|
||||
name: 'endDate',
|
||||
type: 'date',
|
||||
admin: {
|
||||
description:
|
||||
'If "showEndDate" is true, this will define when the event terminates.',
|
||||
condition: (data) => {
|
||||
return data.showEndDate
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'duration',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description:
|
||||
'Text string to display on the event page to indicate the duration of the event. (e.g. "45 mins", "2 days")',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'onDemand',
|
||||
type: 'checkbox',
|
||||
defaultValue: false,
|
||||
admin: {
|
||||
description:
|
||||
'Events that are will remain available on the events page after the event has ended.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'disablePageBuild',
|
||||
type: 'checkbox',
|
||||
defaultValue: false,
|
||||
admin: {
|
||||
description:
|
||||
'When true, the event page will not be built. It will link directly to an external event page (requires Link to be set)',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'link',
|
||||
type: 'group',
|
||||
admin: {
|
||||
description:
|
||||
'Used on event previews to link to a custom page if "disablePageBuild" is true.',
|
||||
condition: (data) => {
|
||||
return data.disablePageBuild
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'href',
|
||||
type: 'text',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'target',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'Same window', value: '_self' },
|
||||
{ label: 'New window', value: '_blank' },
|
||||
],
|
||||
defaultValue: '_blank',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'mainCta',
|
||||
type: 'group',
|
||||
admin: {
|
||||
description: 'Main CTA button on the event page',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'href',
|
||||
type: 'text',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'target',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'Same window', value: '_self' },
|
||||
{ label: 'New window', value: '_blank' },
|
||||
],
|
||||
defaultValue: '_blank',
|
||||
},
|
||||
{
|
||||
name: 'label',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'disabled',
|
||||
type: 'checkbox',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
name: 'disabled_label',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: 'Text for the main CTA button if "mainCta.disabled" is true.',
|
||||
condition: (data) => data.mainCta.disabled,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Participants',
|
||||
fields: [
|
||||
{
|
||||
name: 'company',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'showCompany',
|
||||
type: 'checkbox',
|
||||
defaultValue: false,
|
||||
admin: {
|
||||
description:
|
||||
'If an external company is collaborating with the event, this will display their logo on the event page.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
required: false,
|
||||
admin: {
|
||||
condition: (data) => data.company.showCompany,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'websiteUrl',
|
||||
type: 'text',
|
||||
admin: {
|
||||
condition: (data) => data.company.showCompany,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'logo',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
required: false,
|
||||
admin: {
|
||||
condition: (data) => data.company.showCompany,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'logo_light',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
required: false,
|
||||
admin: {
|
||||
description: 'Light mode logo',
|
||||
condition: (data) => data.company.showCompany,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'participants',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'showParticipants',
|
||||
type: 'checkbox',
|
||||
defaultValue: false,
|
||||
admin: {
|
||||
description:
|
||||
'Could be speakers, authors, guests, etc. It would source from Authors collections.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'participants',
|
||||
type: 'relationship',
|
||||
relationTo: 'authors',
|
||||
hasMany: true,
|
||||
admin: {
|
||||
condition: (data) => data.participants.showParticipants,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'meta',
|
||||
label: 'SEO',
|
||||
fields: [
|
||||
OverviewField({
|
||||
titlePath: 'meta.title',
|
||||
descriptionPath: 'meta.description',
|
||||
imagePath: 'meta.image',
|
||||
}),
|
||||
MetaTitleField({
|
||||
hasGenerateFn: true,
|
||||
}),
|
||||
MetaImageField({
|
||||
relationTo: 'media',
|
||||
}),
|
||||
|
||||
MetaDescriptionField({}),
|
||||
PreviewField({
|
||||
// if the `generateUrl` function is configured
|
||||
hasGenerateFn: true,
|
||||
|
||||
// field paths to match the target field for data
|
||||
titlePath: 'meta.title',
|
||||
descriptionPath: 'meta.description',
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'publishedAt',
|
||||
type: 'date',
|
||||
admin: {
|
||||
date: {
|
||||
pickerAppearance: 'dayAndTime',
|
||||
},
|
||||
position: 'sidebar',
|
||||
},
|
||||
hooks: {
|
||||
beforeChange: [
|
||||
({ siblingData, value }) => {
|
||||
if (siblingData._status === 'published' && !value) {
|
||||
return new Date()
|
||||
}
|
||||
return value
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
timestamps: true,
|
||||
hooks: {
|
||||
afterChange: [revalidateEvent],
|
||||
// afterRead: [populateAuthors],
|
||||
afterDelete: [revalidateDelete],
|
||||
},
|
||||
versions: {
|
||||
drafts: {
|
||||
// autosave: {
|
||||
// interval: 100,
|
||||
// },
|
||||
schedulePublish: true,
|
||||
},
|
||||
maxPerDoc: 50,
|
||||
},
|
||||
}
|
||||
|
||||
export default Events
|
||||
@@ -1,71 +0,0 @@
|
||||
import {
|
||||
FixedToolbarFeature,
|
||||
InlineToolbarFeature,
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { isAnyone } from '../access/isAnyone.ts'
|
||||
import { isAuthenticated } from '../access/isAuthenticated.ts'
|
||||
|
||||
export const Media: CollectionConfig = {
|
||||
slug: 'media',
|
||||
access: {
|
||||
create: isAuthenticated,
|
||||
delete: isAuthenticated,
|
||||
read: isAnyone,
|
||||
update: isAuthenticated,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'alt',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'caption',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ rootFeatures }) => {
|
||||
return [...rootFeatures, FixedToolbarFeature(), InlineToolbarFeature()]
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
upload: {
|
||||
adminThumbnail: 'thumbnail',
|
||||
focalPoint: true,
|
||||
imageSizes: [
|
||||
{
|
||||
name: 'thumbnail',
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
name: 'square',
|
||||
width: 500,
|
||||
height: 500,
|
||||
},
|
||||
{
|
||||
name: 'small',
|
||||
width: 600,
|
||||
},
|
||||
{
|
||||
name: 'medium',
|
||||
width: 900,
|
||||
},
|
||||
{
|
||||
name: 'large',
|
||||
width: 1400,
|
||||
},
|
||||
{
|
||||
name: 'xlarge',
|
||||
width: 1920,
|
||||
},
|
||||
{
|
||||
name: 'og',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
crop: 'center',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { CollectionAfterReadHook } from 'payload'
|
||||
|
||||
import type { Author } from '../../../payload-types'
|
||||
|
||||
export const populateAuthors: CollectionAfterReadHook = async ({ doc, req: { payload } }) => {
|
||||
if (doc?.authors && doc?.authors?.length > 0) {
|
||||
const authorDocs: Author[] = []
|
||||
|
||||
for (const author of doc.authors) {
|
||||
try {
|
||||
const authorDoc = await payload.findByID({
|
||||
id: typeof author === 'object' ? author?.id : author,
|
||||
collection: 'authors',
|
||||
depth: 0,
|
||||
})
|
||||
|
||||
if (authorDoc) {
|
||||
authorDocs.push(authorDoc)
|
||||
}
|
||||
|
||||
if (authorDocs.length > 0) {
|
||||
doc.populatedAuthors = authorDocs.map((authorDoc) => ({
|
||||
id: authorDoc.id,
|
||||
name: authorDoc.author,
|
||||
}))
|
||||
}
|
||||
} catch {
|
||||
// swallow error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return doc
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import type { CollectionAfterChangeHook, CollectionAfterDeleteHook } from 'payload'
|
||||
|
||||
// Avoid importing next/cache at module scope so migrations can run in a plain Node env
|
||||
|
||||
import type { Post } from '../../../payload-types'
|
||||
|
||||
export const revalidatePost: CollectionAfterChangeHook<Post> = async ({
|
||||
doc,
|
||||
previousDoc,
|
||||
req: { payload, context },
|
||||
}) => {
|
||||
if (!context.disableRevalidate) {
|
||||
if (doc._status === 'published') {
|
||||
const path = `/posts/${doc.slug}`
|
||||
|
||||
payload.logger.info(`Revalidating post at path: ${path}`)
|
||||
try {
|
||||
const { revalidatePath, revalidateTag } = await import('next/cache')
|
||||
revalidatePath(path)
|
||||
revalidateTag('posts-sitemap')
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// If the post was previously published, we need to revalidate the old path
|
||||
if (previousDoc._status === 'published' && doc._status !== 'published') {
|
||||
const oldPath = `/posts/${previousDoc.slug}`
|
||||
|
||||
payload.logger.info(`Revalidating old post at path: ${oldPath}`)
|
||||
|
||||
try {
|
||||
const { revalidatePath, revalidateTag } = await import('next/cache')
|
||||
revalidatePath(oldPath)
|
||||
revalidateTag('posts-sitemap')
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
export const revalidateDelete: CollectionAfterDeleteHook<Post> = async ({
|
||||
doc,
|
||||
req: { context },
|
||||
}) => {
|
||||
if (!context.disableRevalidate) {
|
||||
const path = `/posts/${doc?.slug}`
|
||||
try {
|
||||
const { revalidatePath, revalidateTag } = await import('next/cache')
|
||||
revalidatePath(path)
|
||||
revalidateTag('posts-sitemap')
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return doc
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
import {
|
||||
MetaDescriptionField,
|
||||
MetaImageField,
|
||||
MetaTitleField,
|
||||
OverviewField,
|
||||
PreviewField,
|
||||
} from '@payloadcms/plugin-seo/fields'
|
||||
import {
|
||||
BlocksFeature,
|
||||
FixedToolbarFeature,
|
||||
HeadingFeature,
|
||||
HorizontalRuleFeature,
|
||||
InlineToolbarFeature,
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { isAnyone } from '../../access/isAnyone.ts'
|
||||
import { isAuthenticated } from '../../access/isAuthenticated.ts'
|
||||
import { Banner } from '../../blocks/Banner/config.ts'
|
||||
import { Code } from '../../blocks/Code/config.ts'
|
||||
import { MediaBlock } from '../../blocks/MediaBlock/config.ts'
|
||||
import { Quote } from '../../blocks/Quote/config.ts'
|
||||
import { YouTube } from '../../blocks/YouTube/config.ts'
|
||||
import { slugField } from '../../fields/slug/index.ts'
|
||||
import { WWW_SITE_ORIGIN } from '../../utilities/constants.ts'
|
||||
import { populateAuthors } from './hooks/populateAuthors.ts'
|
||||
import { revalidateDelete, revalidatePost } from './hooks/revalidatePost.ts'
|
||||
|
||||
const launchweekOptions = [
|
||||
{ label: '6', value: '6' },
|
||||
{ label: '7', value: '7' },
|
||||
{ label: '8', value: '8' },
|
||||
{ label: 'x', value: 'x' },
|
||||
{ label: 'ga', value: 'ga' },
|
||||
{ label: '12', value: '12' },
|
||||
{ label: '13', value: '13' },
|
||||
{ label: '14', value: '14' },
|
||||
{ label: '15', value: '15' },
|
||||
]
|
||||
|
||||
export const Posts: CollectionConfig = {
|
||||
slug: 'posts',
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
defaultColumns: ['title', 'slug', 'updatedAt', 'publishedAt'],
|
||||
preview: (data) => {
|
||||
const baseUrl = WWW_SITE_ORIGIN || 'http://localhost:3000'
|
||||
// Always use the preview route to ensure draft mode is enabled
|
||||
return `${baseUrl}/api-v2/cms/preview?slug=${data?.slug}&secret=${process.env.PREVIEW_SECRET || 'secret'}`
|
||||
},
|
||||
},
|
||||
access: {
|
||||
create: isAuthenticated,
|
||||
delete: isAuthenticated,
|
||||
read: isAnyone,
|
||||
update: isAuthenticated,
|
||||
},
|
||||
defaultPopulate: {
|
||||
title: true,
|
||||
slug: true,
|
||||
categories: true,
|
||||
meta: {
|
||||
image: true,
|
||||
description: true,
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
...slugField(),
|
||||
{
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
label: 'Description / subtitle',
|
||||
admin: {
|
||||
description: 'Appears as subheading in the blog post preview.',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ rootFeatures }) => {
|
||||
return [
|
||||
...rootFeatures,
|
||||
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
|
||||
BlocksFeature({ blocks: [Banner, Code, MediaBlock, Quote, YouTube] }),
|
||||
FixedToolbarFeature(),
|
||||
InlineToolbarFeature(),
|
||||
HorizontalRuleFeature(),
|
||||
]
|
||||
},
|
||||
}),
|
||||
label: false,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
label: 'Content',
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'thumb',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
required: false,
|
||||
admin: {
|
||||
description: 'Will show up as the blog post cover. Required.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'authors',
|
||||
type: 'relationship',
|
||||
relationTo: 'authors',
|
||||
hasMany: true,
|
||||
admin: {
|
||||
description: 'Authors must be one or more. Required.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'categories',
|
||||
type: 'relationship',
|
||||
hasMany: true,
|
||||
relationTo: 'categories',
|
||||
admin: {
|
||||
description: 'Select only one category. Required.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'readingTime',
|
||||
type: 'number',
|
||||
admin: {
|
||||
hidden: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
type: 'relationship',
|
||||
relationTo: 'tags',
|
||||
hasMany: true,
|
||||
admin: {
|
||||
description: 'Tags can be one or more. Optional.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toc_depth',
|
||||
type: 'number',
|
||||
defaultValue: 3,
|
||||
admin: {
|
||||
hidden: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'launchweek',
|
||||
type: 'select',
|
||||
options: launchweekOptions,
|
||||
admin: {
|
||||
description:
|
||||
'Select a launch week to show launch week summary at the bottom of the blog post. Optional.',
|
||||
},
|
||||
},
|
||||
],
|
||||
label: 'Metadata',
|
||||
},
|
||||
{
|
||||
name: 'meta',
|
||||
label: 'SEO',
|
||||
fields: [
|
||||
OverviewField({
|
||||
titlePath: 'meta.title',
|
||||
descriptionPath: 'meta.description',
|
||||
imagePath: 'meta.image',
|
||||
}),
|
||||
MetaTitleField({
|
||||
hasGenerateFn: true,
|
||||
overrides: {
|
||||
admin: {
|
||||
description: 'Defaults to the title of the post, if not set.',
|
||||
},
|
||||
},
|
||||
}),
|
||||
MetaImageField({
|
||||
relationTo: 'media',
|
||||
overrides: {
|
||||
admin: {
|
||||
description: 'Defaults to the "thumb" image, if not set.',
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
MetaDescriptionField({
|
||||
overrides: {
|
||||
admin: {
|
||||
description: 'Defaults to the description of the post, if not set.',
|
||||
},
|
||||
},
|
||||
}),
|
||||
PreviewField({
|
||||
// if the `generateUrl` function is configured
|
||||
hasGenerateFn: true,
|
||||
|
||||
// field paths to match the target field for data
|
||||
titlePath: 'meta.title',
|
||||
descriptionPath: 'meta.description',
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
/**
|
||||
* "publishedAt" is only internal to cms to determine if the blog post is published or not, but it's not used for sorting blog posts in www
|
||||
* */
|
||||
{
|
||||
name: 'publishedAt',
|
||||
type: 'date',
|
||||
admin: {
|
||||
date: {
|
||||
pickerAppearance: 'dayAndTime',
|
||||
},
|
||||
position: 'sidebar',
|
||||
hidden: true,
|
||||
},
|
||||
hooks: {
|
||||
beforeChange: [
|
||||
({ siblingData, value }) => {
|
||||
/**
|
||||
* Set the "date" field to the current date if user doesn't set it
|
||||
*/
|
||||
if (!siblingData.date) {
|
||||
siblingData.date = new Date()
|
||||
}
|
||||
if (siblingData._status === 'published' && !value) {
|
||||
return new Date()
|
||||
}
|
||||
return value
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
/**
|
||||
* "date" is used to determine the chronological order of the blog post in www
|
||||
*/
|
||||
{
|
||||
name: 'date',
|
||||
type: 'date',
|
||||
label: 'Blog Post Date',
|
||||
admin: {
|
||||
date: {
|
||||
pickerAppearance: 'dayAndTime',
|
||||
},
|
||||
description: 'This date will determine the chronological order of the blog post. Required.',
|
||||
position: 'sidebar',
|
||||
},
|
||||
},
|
||||
],
|
||||
timestamps: true,
|
||||
hooks: {
|
||||
afterChange: [revalidatePost],
|
||||
afterRead: [populateAuthors],
|
||||
afterDelete: [revalidateDelete],
|
||||
},
|
||||
versions: {
|
||||
drafts: {
|
||||
// NOTE: disabled autosave as it might overload connections if many users are editing at the same time
|
||||
// autosave: {
|
||||
// interval: 200,
|
||||
// },
|
||||
// TODO: enable schedulePublish to work with cron job
|
||||
// schedulePublish: true,
|
||||
},
|
||||
maxPerDoc: 50,
|
||||
},
|
||||
}
|
||||
|
||||
export default Posts
|
||||
@@ -1,22 +0,0 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Tags: CollectionConfig = {
|
||||
slug: 'tags',
|
||||
admin: {
|
||||
useAsTitle: 'name',
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
],
|
||||
timestamps: true,
|
||||
}
|
||||
|
||||
export default Tags
|
||||
@@ -1,47 +0,0 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { isAdmin, isAdminFieldLevel } from '../access/isAdmin.ts'
|
||||
import { isAdminOrSelf } from '../access/isAdminOrSelf.ts'
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
admin: {
|
||||
useAsTitle: 'email',
|
||||
},
|
||||
auth: true,
|
||||
access: {
|
||||
// Only admins can create users
|
||||
create: isAdmin,
|
||||
// Admins can read all, but any other logged in user can only read themselves
|
||||
read: isAdminOrSelf,
|
||||
// Admins can update all, but any other logged in user can only update themselves
|
||||
update: isAdminOrSelf,
|
||||
// Only admins can delete
|
||||
delete: isAdmin,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'roles',
|
||||
// Save this field to JWT so we can use from `req.user`
|
||||
saveToJWT: true,
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
defaultValue: ['editor'],
|
||||
access: {
|
||||
// Only admins can create or update a value for this field
|
||||
create: isAdminFieldLevel,
|
||||
update: isAdminFieldLevel,
|
||||
},
|
||||
options: [
|
||||
{
|
||||
label: 'Admin',
|
||||
value: 'admin',
|
||||
},
|
||||
{
|
||||
label: 'Editor',
|
||||
value: 'editor',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
@import '~@payloadcms/ui/scss';
|
||||
|
||||
.admin-bar {
|
||||
@include small-break {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { PayloadAdminBarProps, PayloadMeUser } from '@payloadcms/admin-bar'
|
||||
import { PayloadAdminBar } from '@payloadcms/admin-bar'
|
||||
import { useRouter, useSelectedLayoutSegments } from 'next/navigation'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { cn } from '@/utilities/ui'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
import { getClientSideURL } from '@/utilities/getURL'
|
||||
|
||||
const baseClass = 'admin-bar'
|
||||
|
||||
const collectionLabels = {
|
||||
pages: {
|
||||
plural: 'Pages',
|
||||
singular: 'Page',
|
||||
},
|
||||
posts: {
|
||||
plural: 'Posts',
|
||||
singular: 'Post',
|
||||
},
|
||||
projects: {
|
||||
plural: 'Projects',
|
||||
singular: 'Project',
|
||||
},
|
||||
}
|
||||
|
||||
const Title: React.FC = () => <span>Dashboard</span>
|
||||
|
||||
export const AdminBar: React.FC<{
|
||||
adminBarProps?: PayloadAdminBarProps
|
||||
}> = (props) => {
|
||||
const { adminBarProps } = props || {}
|
||||
const segments = useSelectedLayoutSegments()
|
||||
const [show, setShow] = useState(false)
|
||||
const collection = (
|
||||
collectionLabels[segments?.[1] as keyof typeof collectionLabels] ? segments[1] : 'pages'
|
||||
) as keyof typeof collectionLabels
|
||||
const router = useRouter()
|
||||
|
||||
const onAuthChange = React.useCallback((user: PayloadMeUser) => {
|
||||
setShow(Boolean(user?.id))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(baseClass, 'py-2 bg-black text-white', {
|
||||
block: show,
|
||||
hidden: !show,
|
||||
})}
|
||||
>
|
||||
<div className="container">
|
||||
<PayloadAdminBar
|
||||
{...adminBarProps}
|
||||
className="py-2 text-white"
|
||||
classNames={{
|
||||
controls: 'font-medium text-white',
|
||||
logo: 'text-white',
|
||||
user: 'text-white',
|
||||
}}
|
||||
cmsURL={getClientSideURL()}
|
||||
collectionSlug={collection}
|
||||
collectionLabels={{
|
||||
plural: collectionLabels[collection]?.plural || 'Pages',
|
||||
singular: collectionLabels[collection]?.singular || 'Page',
|
||||
}}
|
||||
logo={<Title />}
|
||||
onAuthChange={onAuthChange}
|
||||
onPreviewExit={() => {
|
||||
fetch('/next/exit-preview').then(() => {
|
||||
router.push('/')
|
||||
router.refresh()
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
padding: 0,
|
||||
position: 'relative',
|
||||
zIndex: 'unset',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
@import '~@payloadcms/ui/scss';
|
||||
|
||||
.dashboard .before-dashboard {
|
||||
margin-bottom: base(1.5);
|
||||
|
||||
&__banner {
|
||||
& h4 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__instructions {
|
||||
list-style: decimal;
|
||||
margin-bottom: base(0.5);
|
||||
|
||||
& li {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
& a:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import { Banner } from '@payloadcms/ui/elements/Banner'
|
||||
import React from 'react'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'before-dashboard'
|
||||
|
||||
const BeforeDashboard: React.FC = () => {
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Banner className={`${baseClass}__banner`} type="success">
|
||||
<h4>Welcome to your dashboard!</h4>
|
||||
</Banner>
|
||||
Here's what to do next:
|
||||
<ul className={`${baseClass}__instructions`}>
|
||||
<li>
|
||||
{' with a few pages, posts, and projects to jump-start your new site, then '}
|
||||
<a href="/" target="_blank">
|
||||
visit your website
|
||||
</a>
|
||||
{' to see the results.'}
|
||||
</li>
|
||||
<li>
|
||||
If you created this repo using Payload Cloud, head over to GitHub and clone it to your
|
||||
local machine. It will be under the <i>GitHub Scope</i> that you selected when creating
|
||||
this project.
|
||||
</li>
|
||||
<li>
|
||||
{'Modify your '}
|
||||
<a
|
||||
href="https://payloadcms.com/docs/configuration/collections"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
collections
|
||||
</a>
|
||||
{' and add more '}
|
||||
<a
|
||||
href="https://payloadcms.com/docs/fields/overview"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
fields
|
||||
</a>
|
||||
{' as needed. If you are new to Payload, we also recommend you check out the '}
|
||||
<a
|
||||
href="https://payloadcms.com/docs/getting-started/what-is-payload"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Getting Started
|
||||
</a>
|
||||
{' docs.'}
|
||||
</li>
|
||||
<li>
|
||||
Commit and push your changes to the repository to trigger a redeployment of your project.
|
||||
</li>
|
||||
</ul>
|
||||
{'Pro Tip: This block is a '}
|
||||
<a
|
||||
href="https://payloadcms.com/docs/admin/custom-components/overview#base-component-overrides"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
custom component
|
||||
</a>
|
||||
, you can remove it at any time by updating your <strong>payload.config</strong>.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BeforeDashboard
|
||||
@@ -1,14 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
const BeforeLogin: React.FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
<b>Welcome to your dashboard!</b>
|
||||
{' This is where site admins will log in to manage your website.'}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BeforeLogin
|
||||
@@ -1,88 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import { Media } from '@/components/Media'
|
||||
import type { Post } from '@/payload-types'
|
||||
import { cn } from '@/utilities/ui'
|
||||
import useClickableCard from '@/utilities/useClickableCard'
|
||||
|
||||
export type CardPostData = Pick<Post, 'slug' | 'categories' | 'meta' | 'title'>
|
||||
|
||||
export const Card: React.FC<{
|
||||
alignItems?: 'center'
|
||||
className?: string
|
||||
doc?: CardPostData
|
||||
relationTo?: 'posts'
|
||||
showCategories?: boolean
|
||||
title?: string
|
||||
}> = (props) => {
|
||||
const { card, link } = useClickableCard({})
|
||||
const { className, doc, relationTo, showCategories, title: titleFromProps } = props
|
||||
|
||||
const { slug, categories, meta, title } = doc || {}
|
||||
const { description, image: metaImage } = meta || {}
|
||||
|
||||
const hasCategories = categories && Array.isArray(categories) && categories.length > 0
|
||||
const titleToUse = titleFromProps || title
|
||||
const sanitizedDescription = description?.replace(/\s/g, ' ') // replace non-breaking space with white space
|
||||
const href = `/${relationTo}/${slug}`
|
||||
|
||||
return (
|
||||
<article
|
||||
className={cn(
|
||||
'border border-border rounded-lg overflow-hidden bg-card hover:cursor-pointer',
|
||||
className
|
||||
)}
|
||||
ref={card.ref as React.Ref<HTMLDivElement>}
|
||||
>
|
||||
<div className="relative w-full ">
|
||||
{!metaImage && <div className="">No image</div>}
|
||||
{metaImage && typeof metaImage !== 'string' && <Media resource={metaImage} size="33vw" />}
|
||||
</div>
|
||||
<div className="p-4">
|
||||
{showCategories && hasCategories && (
|
||||
<div className="uppercase text-sm mb-4">
|
||||
{showCategories && hasCategories && (
|
||||
<div>
|
||||
{categories?.map((category, index) => {
|
||||
if (typeof category === 'object') {
|
||||
const { name: nameFromCategory } = category
|
||||
|
||||
const categoryName = nameFromCategory || 'Untitled category'
|
||||
|
||||
const isLast = index === categories.length - 1
|
||||
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
{categoryName}
|
||||
{!isLast && <Fragment>, </Fragment>}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{titleToUse && (
|
||||
<div className="prose">
|
||||
<h3>
|
||||
<Link
|
||||
className="not-prose"
|
||||
href={href}
|
||||
ref={link.ref as React.Ref<HTMLAnchorElement>}
|
||||
>
|
||||
{titleToUse}
|
||||
</Link>
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
{description && <div className="mt-2">{description && <p>{sanitizedDescription}</p>}</div>}
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import { Card, CardPostData } from '@/components/Card'
|
||||
import { cn } from '@/utilities/ui'
|
||||
|
||||
export type Props = {
|
||||
posts: CardPostData[]
|
||||
}
|
||||
|
||||
export const CollectionArchive: React.FC<Props> = (props) => {
|
||||
const { posts } = props
|
||||
|
||||
return (
|
||||
<div className={cn('container')}>
|
||||
<div>
|
||||
<div className="grid grid-cols-4 sm:grid-cols-8 lg:grid-cols-12 gap-y-4 gap-x-4 lg:gap-y-8 lg:gap-x-8 xl:gap-x-8">
|
||||
{posts?.map((result, index) => {
|
||||
if (typeof result === 'object' && result !== null) {
|
||||
return (
|
||||
<div className="col-span-4" key={index}>
|
||||
<Card className="h-full" doc={result} relationTo="posts" showCategories />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
import { Button, type ButtonProps } from '@/components/ui/button'
|
||||
import type { Post } from '@/payload-types'
|
||||
import { cn } from '@/utilities/ui'
|
||||
|
||||
type CMSLinkType = {
|
||||
appearance?: 'inline' | ButtonProps['variant']
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
label?: string | null
|
||||
newTab?: boolean | null
|
||||
reference?: {
|
||||
relationTo: 'posts'
|
||||
value: Post | string | number
|
||||
} | null
|
||||
size?: ButtonProps['size'] | null
|
||||
type?: 'custom' | 'reference' | null
|
||||
url?: string | null
|
||||
}
|
||||
|
||||
export const CMSLink: React.FC<CMSLinkType> = (props) => {
|
||||
const {
|
||||
type,
|
||||
appearance = 'inline',
|
||||
children,
|
||||
className,
|
||||
label,
|
||||
newTab,
|
||||
reference,
|
||||
size: sizeFromProps,
|
||||
url,
|
||||
} = props
|
||||
|
||||
const href =
|
||||
type === 'reference' && typeof reference?.value === 'object' && reference.value.slug
|
||||
? `/${reference.value.slug}`
|
||||
: url
|
||||
|
||||
if (!href) return null
|
||||
|
||||
const size = appearance === 'link' ? 'clear' : sizeFromProps
|
||||
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
|
||||
|
||||
/* Ensure we don't break any styles set by richText */
|
||||
if (appearance === 'inline') {
|
||||
return (
|
||||
<Link className={cn(className)} href={href || url || ''} {...newTabProps}>
|
||||
{label && label}
|
||||
{children && children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button asChild className={className} size={size} variant={appearance}>
|
||||
<Link className={cn(className)} href={href || url || ''} {...newTabProps}>
|
||||
{label && label}
|
||||
{children && children}
|
||||
</Link>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { RefreshRouteOnSave as PayloadLivePreview } from '@payloadcms/live-preview-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import React from 'react'
|
||||
|
||||
import { getClientSideURL } from '@/utilities/getURL'
|
||||
|
||||
export const LivePreviewListener: React.FC = () => {
|
||||
const router = useRouter()
|
||||
return <PayloadLivePreview refresh={router.refresh} serverURL={getClientSideURL()} />
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import clsx from 'clsx'
|
||||
import React from 'react'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
loading?: 'lazy' | 'eager'
|
||||
priority?: 'auto' | 'high' | 'low'
|
||||
}
|
||||
|
||||
export const Logo = (props: Props) => {
|
||||
const { loading: loadingFromProps, priority: priorityFromProps, className } = props
|
||||
|
||||
const loading = loadingFromProps || 'lazy'
|
||||
const priority = priorityFromProps || 'low'
|
||||
|
||||
return (
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
<img
|
||||
alt="Supabase Logo"
|
||||
width={193}
|
||||
height={34}
|
||||
loading={loading}
|
||||
fetchPriority={priority}
|
||||
decoding="async"
|
||||
className={clsx('max-w-[9.375rem] w-full h-[34px]', className)}
|
||||
src="https://supabase.com/_next/image?url=https%3A%2F%2Ffrontend-assets.supabase.com%2Fwww%2F34022bd5708c%2F_next%2Fstatic%2Fmedia%2Fsupabase-logo-wordmark--dark.b36ebb5f.png&w=256&q=75&dpl=dpl_EbjEEHsJWQ5nbTaMsQgJBoZ7tBCD"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { StaticImageData } from 'next/image'
|
||||
import NextImage from 'next/image'
|
||||
import React from 'react'
|
||||
|
||||
import type { Props as MediaProps } from '../types'
|
||||
import { cssVariables } from '@/cssVariables'
|
||||
import { getClientSideURL } from '@/utilities/getURL'
|
||||
import { cn } from '@/utilities/ui'
|
||||
|
||||
const { breakpoints } = cssVariables
|
||||
|
||||
// A base64 encoded image to use as a placeholder while the image is loading
|
||||
const placeholderBlur =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAABchJREFUWEdtlwtTG0kMhHtGM7N+AAdcDsjj///EBLzenbtuadbLJaZUTlHB+tRqSesETB3IABqQG1KbUFqDlQorBSmboqeEBcC1d8zrCixXYGZcgMsFmH8B+AngHdurAmXKOE8nHOoBrU6opcGswPi5KSP9CcBaQ9kACJH/ALAA1xm4zMD8AczvQCcAQeJVAZsy7nYApTSUzwCHUKACeUJi9TsFci7AHmDtuHYqQIC9AgQYKnSwNAig4NyOOwXq/xU47gDYggarjIpsRSEA3Fqw7AGkwgW4fgALAdiC2btKgNZwbgdMbEFpqFR2UyCR8xwAhf8bUHIGk1ckMyB5C1YkeWAdAPQBAeiD6wVYPoD1HUgXwFagZAGc6oSpTmilopoD5GzISQD3odcNIFca0BUQQM5YA2DpHV0AYURBDIAL0C+ugC0C4GedSsVUmwC8/4w8TPiwU6AClJ5RWL1PgQNkrABWdKB3YF3cBwRY5lsI4ApkKpCQi+FIgFJU/TDgDuAxAAwonJuKpGD1rkCXCR1ALyrAUSSEQAhwBdYZ6DPAgSUA2c1wKIZmRcHxMzMYR9DH8NlbkAwwApSAcABwBwTAbb6owAr0AFiZPILVEyCtMmK2jCkTwFDNUNj7nJETQx744gCUmgkZVGJUHyakEZE4W91jtGFA9KsD8Z3JFYDlhGYZLWcllwJMnplcPy+csFAgAAaIDOgeuAGoB96GLZg4kmtfMjnr6ig5oSoySsoy3ya/FMivXZWxwr0KIf9nACbfqcBEgmBSAtAlIT83R+70IWpyACamIjf5E1Iqb9ECVmnoI/FvAIRk8s2J0Y5IquQDgB+5wpScw5AUTC75VTmTs+72NUzoCvQIaAXv5Q8PDAZKLD+MxLv3RFE7KlsQChgBIlKiCv5ByaZv3gJZNm8AnVMhAN+EjrtTYQMICJpu6/0aiQnhClANlz+Bw0cIWa8ev0sBrtrhAyaXEnrfGfATQJiRKih5vKeOHNXXPFrgyamAADh0Q4F2/sESojomDS9o9k0b0H83xjB8qL+JNoTjN+enjpaBpingRh4e8MSugudM030A8FeqMI6PFIgNyPehkpZWGFEAARIQdH5LcAAqIACHkAJqg4OoBccHAuz76wr4BbzFOEa8iBuAZB8AtJHLP2VgMgJw/EIBowo7HxCAH3V6dAXEE/vZ5aZIA8BP8RKhm7Cp8BnAMnAQADdgQDA520AVIpScP+enHz0Gwp25h4i2dPg5FkDXrbsdJikQwXuWgaM5gEMk1AgH4DKKFjDf3bMD+FjEeIxLlRKYnBk2BbquvSDCAQ4gwZiMAAmH4gBTyRtEsYxi7gP6QSrc//39BrDNqG8rtYTmC4BV1SfMhOhaumFCT87zy4pPhQBZEK1kQVRjJBBi7AOlePgyAPYjwlvtagx9e/dnQraAyS894TIkkAIEYMKEc8k4EqJ68lZ5jjNqcQC2QteQOf7659umwBgPybNtK4dg9WvnMyFwXYGP7uEO1lwJgAnPNeMYMVXbIIYKFioI4PGFt+BWPVfmWJdjW2lTUnLGCswECAgaUy86iwA1464ajo0QhgMBFGyBoZahANsMpMfXr1JA1SN29m5lqgXj+UPV85uRA7yv/KYUO4Tk7Hc1AZwbIRzg0AyNj2UlAMwfSLSMnl7fdAbcxHuA27YaAMvaQ4GOjwX4RTUGAG8Ge14N963g1AynqUiFqRX9noasxT4b8entNRQYyamk/3tYcHsO7R3XJRRYOn4tw4iUnwBM5gDnySGOreAwAGo8F9IDHEcq8Pz2Kg/oXCpuIL6tOPD8LsDn0ABYQoGFRowlsAEUPPDrGAGowAbgKsgDMmE8mDy/vXQ9IAwI7u4wta+gAdAdgB64Ah9SgD4IgGKhwACoAjgNgFDhtxY8f33ZTMjqdTAiHMBPrn8ZWkEfzFdX4Oc1AHg3+ADbvN8PU8WdFKg4Tt6CQy2+D4YHaMT/JP4XzbAq98cPDIUAAAAASUVORK5CYII='
|
||||
|
||||
export const ImageMedia: React.FC<MediaProps> = (props) => {
|
||||
const {
|
||||
alt: altFromProps,
|
||||
fill,
|
||||
pictureClassName,
|
||||
imgClassName,
|
||||
priority,
|
||||
resource,
|
||||
size: sizeFromProps,
|
||||
src: srcFromProps,
|
||||
loading: loadingFromProps,
|
||||
} = props
|
||||
|
||||
let width: number | undefined
|
||||
let height: number | undefined
|
||||
let alt = altFromProps
|
||||
let src: StaticImageData | string = srcFromProps || ''
|
||||
|
||||
if (!src && resource && typeof resource === 'object') {
|
||||
const { alt: altFromResource, height: fullHeight, url, width: fullWidth } = resource
|
||||
|
||||
width = fullWidth!
|
||||
height = fullHeight!
|
||||
alt = altFromResource || ''
|
||||
|
||||
const cacheTag = resource.updatedAt
|
||||
|
||||
src = `${getClientSideURL()}${url}?${cacheTag}`
|
||||
}
|
||||
|
||||
const loading = loadingFromProps || (!priority ? 'lazy' : undefined)
|
||||
|
||||
// NOTE: this is used by the browser to determine which image to download at different screen sizes
|
||||
const sizes = sizeFromProps
|
||||
? sizeFromProps
|
||||
: Object.entries(breakpoints)
|
||||
.map(([, value]) => `(max-width: ${value}px) ${value * 2}w`)
|
||||
.join(', ')
|
||||
|
||||
return (
|
||||
<picture className={cn(pictureClassName)}>
|
||||
<NextImage
|
||||
alt={alt || ''}
|
||||
className={cn(imgClassName)}
|
||||
fill={fill}
|
||||
height={!fill ? height : undefined}
|
||||
placeholder="blur"
|
||||
blurDataURL={placeholderBlur}
|
||||
priority={priority}
|
||||
quality={100}
|
||||
loading={loading}
|
||||
sizes={sizes}
|
||||
src={src}
|
||||
width={!fill ? width : undefined}
|
||||
/>
|
||||
</picture>
|
||||
)
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
|
||||
import type { Props as MediaProps } from '../types'
|
||||
import { getClientSideURL } from '@/utilities/getURL'
|
||||
import { cn } from '@/utilities/ui'
|
||||
|
||||
export const VideoMedia: React.FC<MediaProps> = (props) => {
|
||||
const { onClick, resource, videoClassName } = props
|
||||
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
// const [showFallback] = useState<boolean>()
|
||||
|
||||
useEffect(() => {
|
||||
const { current: video } = videoRef
|
||||
if (video) {
|
||||
video.addEventListener('suspend', () => {
|
||||
// setShowFallback(true);
|
||||
// console.warn('Video was suspended, rendering fallback image.')
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (resource && typeof resource === 'object') {
|
||||
const { filename } = resource
|
||||
|
||||
return (
|
||||
<video
|
||||
autoPlay
|
||||
className={cn(videoClassName)}
|
||||
controls={false}
|
||||
loop
|
||||
muted
|
||||
onClick={onClick}
|
||||
playsInline
|
||||
ref={videoRef}
|
||||
>
|
||||
<source src={`${getClientSideURL()}/media/${filename}`} />
|
||||
</video>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import { ImageMedia } from './ImageMedia'
|
||||
import type { Props } from './types'
|
||||
import { VideoMedia } from './VideoMedia'
|
||||
|
||||
export const Media: React.FC<Props> = (props) => {
|
||||
const { className, htmlElement = 'div', resource } = props
|
||||
|
||||
const isVideo = typeof resource === 'object' && resource?.mimeType?.includes('video')
|
||||
const Tag = htmlElement || Fragment
|
||||
|
||||
return (
|
||||
<Tag
|
||||
{...(htmlElement !== null
|
||||
? {
|
||||
className,
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
{isVideo ? <VideoMedia {...props} /> : <ImageMedia {...props} />}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import type { StaticImageData } from 'next/image'
|
||||
import type { ElementType, Ref } from 'react'
|
||||
|
||||
import type { Media as MediaType } from '@/payload-types'
|
||||
|
||||
export interface Props {
|
||||
alt?: string
|
||||
className?: string
|
||||
fill?: boolean // for NextImage only
|
||||
htmlElement?: ElementType | null
|
||||
pictureClassName?: string
|
||||
imgClassName?: string
|
||||
onClick?: () => void
|
||||
onLoad?: () => void
|
||||
loading?: 'lazy' | 'eager' // for NextImage only
|
||||
priority?: boolean // for NextImage only
|
||||
ref?: Ref<HTMLImageElement | HTMLVideoElement | null>
|
||||
resource?: MediaType | string | number | null // for Payload media
|
||||
size?: string // for NextImage only
|
||||
src?: StaticImageData // for static media
|
||||
videoClassName?: string
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
const defaultLabels = {
|
||||
plural: 'Docs',
|
||||
singular: 'Doc',
|
||||
}
|
||||
|
||||
const defaultCollectionLabels = {
|
||||
posts: {
|
||||
plural: 'Posts',
|
||||
singular: 'Post',
|
||||
},
|
||||
}
|
||||
|
||||
export const PageRange: React.FC<{
|
||||
className?: string
|
||||
collection?: keyof typeof defaultCollectionLabels
|
||||
collectionLabels?: {
|
||||
plural?: string
|
||||
singular?: string
|
||||
}
|
||||
currentPage?: number
|
||||
limit?: number
|
||||
totalDocs?: number
|
||||
}> = (props) => {
|
||||
const {
|
||||
className,
|
||||
collection,
|
||||
collectionLabels: collectionLabelsFromProps,
|
||||
currentPage,
|
||||
limit,
|
||||
totalDocs,
|
||||
} = props
|
||||
|
||||
let indexStart = (currentPage ? currentPage - 1 : 1) * (limit || 1) + 1
|
||||
if (totalDocs && indexStart > totalDocs) indexStart = 0
|
||||
|
||||
let indexEnd = (currentPage || 1) * (limit || 1)
|
||||
if (totalDocs && indexEnd > totalDocs) indexEnd = totalDocs
|
||||
|
||||
const { plural, singular } =
|
||||
collectionLabelsFromProps ||
|
||||
(collection ? defaultCollectionLabels[collection] : undefined) ||
|
||||
defaultLabels ||
|
||||
{}
|
||||
|
||||
return (
|
||||
<div className={[className, 'font-semibold'].filter(Boolean).join(' ')}>
|
||||
{(typeof totalDocs === 'undefined' || totalDocs === 0) && 'Search produced no results.'}
|
||||
{typeof totalDocs !== 'undefined' &&
|
||||
totalDocs > 0 &&
|
||||
`Showing ${indexStart}${indexStart > 0 ? ` - ${indexEnd}` : ''} of ${totalDocs} ${
|
||||
totalDocs > 1 ? plural : singular
|
||||
}`}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
import React from 'react'
|
||||
|
||||
import {
|
||||
Pagination as PaginationComponent,
|
||||
PaginationContent,
|
||||
PaginationEllipsis,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from '@/components/ui/pagination'
|
||||
import { cn } from '@/utilities/ui'
|
||||
|
||||
export const Pagination: React.FC<{
|
||||
className?: string
|
||||
page: number
|
||||
totalPages: number
|
||||
}> = (props) => {
|
||||
const router = useRouter()
|
||||
|
||||
const { className, page, totalPages } = props
|
||||
const hasNextPage = page < totalPages
|
||||
const hasPrevPage = page > 1
|
||||
|
||||
const hasExtraPrevPages = page - 1 > 1
|
||||
const hasExtraNextPages = page + 1 < totalPages
|
||||
|
||||
return (
|
||||
<div className={cn('my-12', className)}>
|
||||
<PaginationComponent>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
disabled={!hasPrevPage}
|
||||
onClick={() => {
|
||||
router.push(`/posts/page/${page - 1}`)
|
||||
}}
|
||||
/>
|
||||
</PaginationItem>
|
||||
|
||||
{hasExtraPrevPages && (
|
||||
<PaginationItem>
|
||||
<PaginationEllipsis />
|
||||
</PaginationItem>
|
||||
)}
|
||||
|
||||
{hasPrevPage && (
|
||||
<PaginationItem>
|
||||
<PaginationLink
|
||||
onClick={() => {
|
||||
router.push(`/posts/page/${page - 1}`)
|
||||
}}
|
||||
>
|
||||
{page - 1}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
)}
|
||||
|
||||
<PaginationItem>
|
||||
<PaginationLink
|
||||
isActive
|
||||
onClick={() => {
|
||||
router.push(`/posts/page/${page}`)
|
||||
}}
|
||||
>
|
||||
{page}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
|
||||
{hasNextPage && (
|
||||
<PaginationItem>
|
||||
<PaginationLink
|
||||
onClick={() => {
|
||||
router.push(`/posts/page/${page + 1}`)
|
||||
}}
|
||||
>
|
||||
{page + 1}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
)}
|
||||
|
||||
{hasExtraNextPages && (
|
||||
<PaginationItem>
|
||||
<PaginationEllipsis />
|
||||
</PaginationItem>
|
||||
)}
|
||||
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
disabled={!hasNextPage}
|
||||
onClick={() => {
|
||||
router.push(`/posts/page/${page + 1}`)
|
||||
}}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</PaginationComponent>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import {
|
||||
DefaultNodeTypes,
|
||||
SerializedBlockNode,
|
||||
SerializedLinkNode,
|
||||
type DefaultTypedEditorState,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import {
|
||||
RichText as ConvertRichText,
|
||||
JSXConvertersFunction,
|
||||
LinkJSXConverter,
|
||||
} from '@payloadcms/richtext-lexical/react'
|
||||
|
||||
import { BannerBlock } from '@/blocks/Banner/Component'
|
||||
import { CodeBlock, CodeBlockProps } from '@/blocks/Code/Component'
|
||||
import { MediaBlock } from '@/blocks/MediaBlock/Component'
|
||||
import { QuoteBlock } from '@/blocks/Quote/Component'
|
||||
import { YouTubeBlock } from '@/blocks/YouTube/Component'
|
||||
import type {
|
||||
BannerBlock as BannerBlockProps,
|
||||
MediaBlock as MediaBlockProps,
|
||||
QuoteBlock as QuoteBlockProps,
|
||||
YouTubeBlock as YouTubeBlockProps,
|
||||
} from '@/payload-types'
|
||||
import { cn } from '@/utilities/ui'
|
||||
|
||||
type NodeTypes =
|
||||
| DefaultNodeTypes
|
||||
| SerializedBlockNode<
|
||||
MediaBlockProps | BannerBlockProps | CodeBlockProps | QuoteBlockProps | YouTubeBlockProps
|
||||
>
|
||||
|
||||
const internalDocToHref = ({ linkNode }: { linkNode: SerializedLinkNode }) => {
|
||||
const { value, relationTo } = linkNode.fields.doc!
|
||||
if (typeof value !== 'object') {
|
||||
throw new Error('Expected value to be an object')
|
||||
}
|
||||
const slug = value.slug
|
||||
return relationTo === 'posts' ? `/posts/${slug}` : `/${slug}`
|
||||
}
|
||||
|
||||
const jsxConverters: JSXConvertersFunction<NodeTypes> = ({ defaultConverters }) => ({
|
||||
...defaultConverters,
|
||||
...LinkJSXConverter({ internalDocToHref }),
|
||||
blocks: {
|
||||
banner: ({ node }) => <BannerBlock className="col-start-2 mb-4" {...node.fields} />,
|
||||
mediaBlock: ({ node }) => (
|
||||
<MediaBlock
|
||||
className="col-start-1 col-span-3"
|
||||
imgClassName="m-0"
|
||||
{...node.fields}
|
||||
captionClassName="mx-auto max-w-[48rem]"
|
||||
enableGutter={false}
|
||||
disableInnerContainer={true}
|
||||
/>
|
||||
),
|
||||
code: ({ node }) => <CodeBlock className="col-start-2" {...node.fields} />,
|
||||
quote: ({ node }) => <QuoteBlock className="col-start-2" {...node.fields} />,
|
||||
youtube: ({ node }) => <YouTubeBlock className="col-start-2" {...node.fields} />,
|
||||
},
|
||||
})
|
||||
|
||||
type Props = {
|
||||
data: DefaultTypedEditorState
|
||||
enableGutter?: boolean
|
||||
enableProse?: boolean
|
||||
} & React.HTMLAttributes<HTMLDivElement>
|
||||
|
||||
export default function RichText(props: Props) {
|
||||
const { className, enableProse = true, enableGutter = true, ...rest } = props
|
||||
return (
|
||||
<ConvertRichText
|
||||
converters={jsxConverters}
|
||||
className={cn(
|
||||
'payload-richtext',
|
||||
{
|
||||
container: enableGutter,
|
||||
'max-w-none': !enableGutter,
|
||||
'mx-auto prose md:prose-md dark:prose-invert': enableProse,
|
||||
},
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/utilities/ui'
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
defaultVariants: {
|
||||
size: 'default',
|
||||
variant: 'default',
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
clear: '',
|
||||
default: 'h-10 px-4 py-2',
|
||||
icon: 'h-10 w-10',
|
||||
lg: 'h-11 rounded px-8',
|
||||
sm: 'h-9 rounded px-3',
|
||||
},
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
ghost: 'hover:bg-card hover:text-accent-foreground',
|
||||
link: 'text-primary items-start justify-start underline-offset-4 hover:underline',
|
||||
outline: 'border border-border bg-background hover:bg-card hover:text-accent-foreground',
|
||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
ref?: React.Ref<HTMLButtonElement>
|
||||
}
|
||||
|
||||
const Button: React.FC<ButtonProps> = ({
|
||||
asChild = false,
|
||||
className,
|
||||
size,
|
||||
variant,
|
||||
ref,
|
||||
...props
|
||||
}) => {
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
return <Comp className={cn(buttonVariants({ className, size, variant }))} ref={ref} {...props} />
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -1,49 +0,0 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/utilities/ui'
|
||||
|
||||
const Card: React.FC<
|
||||
{ ref?: React.Ref<HTMLDivElement> } & React.HTMLAttributes<HTMLDivElement>
|
||||
> = ({ className, ref, ...props }) => (
|
||||
<div
|
||||
className={cn('rounded-lg border bg-card text-card-foreground shadow-sm', className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
const CardHeader: React.FC<
|
||||
{ ref?: React.Ref<HTMLDivElement> } & React.HTMLAttributes<HTMLDivElement>
|
||||
> = ({ className, ref, ...props }) => (
|
||||
<div className={cn('flex flex-col space-y-1.5 p-6', className)} ref={ref} {...props} />
|
||||
)
|
||||
|
||||
const CardTitle: React.FC<
|
||||
{ ref?: React.Ref<HTMLHeadingElement> } & React.HTMLAttributes<HTMLHeadingElement>
|
||||
> = ({ className, ref, ...props }) => (
|
||||
<h3
|
||||
className={cn('text-2xl font-semibold leading-none tracking-tight', className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
const CardDescription: React.FC<
|
||||
{ ref?: React.Ref<HTMLParagraphElement> } & React.HTMLAttributes<HTMLParagraphElement>
|
||||
> = ({ className, ref, ...props }) => (
|
||||
<p className={cn('text-sm text-muted-foreground', className)} ref={ref} {...props} />
|
||||
)
|
||||
|
||||
const CardContent: React.FC<
|
||||
{ ref?: React.Ref<HTMLDivElement> } & React.HTMLAttributes<HTMLDivElement>
|
||||
> = ({ className, ref, ...props }) => (
|
||||
<div className={cn('p-6 pt-0', className)} ref={ref} {...props} />
|
||||
)
|
||||
|
||||
const CardFooter: React.FC<
|
||||
{ ref?: React.Ref<HTMLDivElement> } & React.HTMLAttributes<HTMLDivElement>
|
||||
> = ({ className, ref, ...props }) => (
|
||||
<div className={cn('flex items-center p-6 pt-0', className)} ref={ref} {...props} />
|
||||
)
|
||||
|
||||
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
|
||||
@@ -1,28 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
|
||||
import { Check } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/utilities/ui'
|
||||
|
||||
const Checkbox: React.FC<
|
||||
{
|
||||
ref?: React.Ref<HTMLButtonElement>
|
||||
} & React.ComponentProps<typeof CheckboxPrimitive.Root>
|
||||
> = ({ className, ref, ...props }) => (
|
||||
<CheckboxPrimitive.Root
|
||||
className={cn(
|
||||
'peer h-4 w-4 shrink-0 rounded border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator className={cn('flex items-center justify-center text-current')}>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
|
||||
export { Checkbox }
|
||||
@@ -1,23 +0,0 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/utilities/ui'
|
||||
|
||||
const Input: React.FC<
|
||||
{
|
||||
ref?: React.Ref<HTMLInputElement>
|
||||
} & React.InputHTMLAttributes<HTMLInputElement>
|
||||
> = ({ type, className, ref, ...props }) => {
|
||||
return (
|
||||
<input
|
||||
className={cn(
|
||||
'flex h-10 w-full rounded border border-border bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
type={type}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
@@ -1,20 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as LabelPrimitive from '@radix-ui/react-label'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/utilities/ui'
|
||||
|
||||
const labelVariants = cva(
|
||||
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
|
||||
)
|
||||
|
||||
const Label: React.FC<
|
||||
{ ref?: React.Ref<HTMLLabelElement> } & React.ComponentProps<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
> = ({ className, ref, ...props }) => (
|
||||
<LabelPrimitive.Root className={cn(labelVariants(), className)} ref={ref} {...props} />
|
||||
)
|
||||
|
||||
export { Label }
|
||||
@@ -1,92 +0,0 @@
|
||||
import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
|
||||
import type { ButtonProps } from '@/components/ui/button'
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
import { cn } from '@/utilities/ui'
|
||||
|
||||
const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
|
||||
<nav
|
||||
aria-label="pagination"
|
||||
className={cn('mx-auto flex w-full justify-center', className)}
|
||||
role="navigation"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
const PaginationContent: React.FC<
|
||||
{ ref?: React.Ref<HTMLUListElement> } & React.HTMLAttributes<HTMLUListElement>
|
||||
> = ({ className, ref, ...props }) => (
|
||||
<ul className={cn('flex flex-row items-center gap-1', className)} ref={ref} {...props} />
|
||||
)
|
||||
|
||||
const PaginationItem: React.FC<
|
||||
{ ref?: React.Ref<HTMLLIElement> } & React.HTMLAttributes<HTMLLIElement>
|
||||
> = ({ className, ref, ...props }) => <li className={cn('', className)} ref={ref} {...props} />
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean
|
||||
} & Pick<ButtonProps, 'size'> &
|
||||
React.ComponentProps<'button'>
|
||||
|
||||
const PaginationLink = ({ className, isActive, size = 'icon', ...props }: PaginationLinkProps) => (
|
||||
<button
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
size,
|
||||
variant: isActive ? 'outline' : 'ghost',
|
||||
}),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
const PaginationPrevious = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
className={cn('gap-1 pl-2.5', className)}
|
||||
size="default"
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
<span>Previous</span>
|
||||
</PaginationLink>
|
||||
)
|
||||
|
||||
const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
className={cn('gap-1 pr-2.5', className)}
|
||||
size="default"
|
||||
{...props}
|
||||
>
|
||||
<span>Next</span>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</PaginationLink>
|
||||
)
|
||||
|
||||
const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<'span'>) => (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn('flex h-9 w-9 items-center justify-center', className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
)
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationEllipsis,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as SelectPrimitive from '@radix-ui/react-select'
|
||||
import { Check, ChevronDown, ChevronUp } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/utilities/ui'
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
|
||||
const SelectTrigger: React.FC<
|
||||
{ ref?: React.Ref<HTMLButtonElement> } & React.ComponentProps<typeof SelectPrimitive.Trigger>
|
||||
> = ({ children, className, ref, ...props }) => (
|
||||
<SelectPrimitive.Trigger
|
||||
className={cn(
|
||||
'flex h-10 w-full items-center justify-between rounded border border-input bg-background px-3 py-2 text-inherit ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
|
||||
const SelectScrollUpButton: React.FC<
|
||||
{ ref?: React.Ref<HTMLDivElement> } & React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>
|
||||
> = ({ className, ref, ...props }) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
className={cn('flex cursor-default items-center justify-center py-1', className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
|
||||
const SelectScrollDownButton: React.FC<
|
||||
{ ref?: React.Ref<HTMLDivElement> } & React.ComponentProps<
|
||||
typeof SelectPrimitive.ScrollDownButton
|
||||
>
|
||||
> = ({ className, ref, ...props }) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
className={cn('flex cursor-default items-center justify-center py-1', className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
|
||||
const SelectContent: React.FC<
|
||||
{
|
||||
ref?: React.Ref<HTMLDivElement>
|
||||
} & React.ComponentProps<typeof SelectPrimitive.Content>
|
||||
> = ({ children, className, position = 'popper', ref, ...props }) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
className={cn(
|
||||
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded border bg-card text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
position === 'popper' &&
|
||||
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
'p-1',
|
||||
position === 'popper' &&
|
||||
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
|
||||
const SelectLabel: React.FC<
|
||||
{ ref?: React.Ref<HTMLDivElement> } & React.ComponentProps<typeof SelectPrimitive.Label>
|
||||
> = ({ className, ref, ...props }) => (
|
||||
<SelectPrimitive.Label
|
||||
className={cn('py-1.5 pl-8 pr-2 text-sm font-semibold', className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
const SelectItem: React.FC<
|
||||
{ ref?: React.Ref<HTMLDivElement>; value: string } & React.ComponentProps<
|
||||
typeof SelectPrimitive.Item
|
||||
>
|
||||
> = ({ children, className, ref, ...props }) => (
|
||||
<SelectPrimitive.Item
|
||||
className={cn(
|
||||
'relative flex w-full cursor-default select-none items-center rounded py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
|
||||
const SelectSeparator: React.FC<
|
||||
{ ref?: React.Ref<HTMLDivElement> } & React.ComponentProps<typeof SelectPrimitive.Separator>
|
||||
> = ({ className, ref, ...props }) => (
|
||||
<SelectPrimitive.Separator
|
||||
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/utilities/ui'
|
||||
|
||||
const Textarea: React.FC<
|
||||
{
|
||||
ref?: React.Ref<HTMLTextAreaElement>
|
||||
} & React.TextareaHTMLAttributes<HTMLTextAreaElement>
|
||||
> = ({ className, ref, ...props }) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
'flex min-h-[80px] w-full rounded border border-border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Textarea }
|
||||
@@ -1,12 +0,0 @@
|
||||
// Keep these in sync with the CSS variables in your tailwind configuration
|
||||
|
||||
export const cssVariables = {
|
||||
breakpoints: {
|
||||
'3xl': 1920,
|
||||
'2xl': 1536,
|
||||
xl: 1280,
|
||||
lg: 1024,
|
||||
md: 768,
|
||||
sm: 640,
|
||||
},
|
||||
}
|
||||
Vendored
-14
@@ -1,14 +0,0 @@
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
PAYLOAD_SECRET: string
|
||||
DATABASE_URI: string
|
||||
WWW_SITE_ORIGIN: string
|
||||
VERCEL_PROJECT_PRODUCTION_URL: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If this file has no import/export statements (i.e. is a script)
|
||||
// convert it into a module by adding an empty export statement.
|
||||
export {}
|
||||
@@ -1,47 +0,0 @@
|
||||
import {
|
||||
BoldFeature,
|
||||
ItalicFeature,
|
||||
lexicalEditor,
|
||||
LinkFeature,
|
||||
ParagraphFeature,
|
||||
UnderlineFeature,
|
||||
type LinkFields,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import type { TextFieldSingleValidation } from 'payload'
|
||||
|
||||
export const defaultLexical = lexicalEditor({
|
||||
features: [
|
||||
ParagraphFeature(),
|
||||
UnderlineFeature(),
|
||||
BoldFeature(),
|
||||
ItalicFeature(),
|
||||
LinkFeature({
|
||||
enabledCollections: ['posts'],
|
||||
fields: ({ defaultFields }) => {
|
||||
const defaultFieldsWithoutUrl = defaultFields.filter((field) => {
|
||||
if ('name' in field && field.name === 'url') return false
|
||||
return true
|
||||
})
|
||||
|
||||
return [
|
||||
...defaultFieldsWithoutUrl,
|
||||
{
|
||||
name: 'url',
|
||||
type: 'text',
|
||||
admin: {
|
||||
condition: (_data, siblingData) => siblingData?.linkType !== 'internal',
|
||||
},
|
||||
label: ({ t }) => t('fields:enterURL'),
|
||||
required: true,
|
||||
validate: ((value, options) => {
|
||||
if ((options?.siblingData as LinkFields)?.linkType === 'internal') {
|
||||
return true // no validation needed, as no url should exist for internal links
|
||||
}
|
||||
return value ? true : 'URL is required'
|
||||
}) as TextFieldSingleValidation,
|
||||
},
|
||||
]
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
@@ -1,139 +0,0 @@
|
||||
import type { Field, GroupField } from 'payload'
|
||||
|
||||
import deepMerge from '../utilities/deepMerge.ts'
|
||||
|
||||
export type LinkAppearances = 'default' | 'outline'
|
||||
|
||||
export const appearanceOptions: Record<LinkAppearances, { label: string; value: string }> = {
|
||||
default: {
|
||||
label: 'Default',
|
||||
value: 'default',
|
||||
},
|
||||
outline: {
|
||||
label: 'Outline',
|
||||
value: 'outline',
|
||||
},
|
||||
}
|
||||
|
||||
type LinkType = (options?: {
|
||||
appearances?: LinkAppearances[] | false
|
||||
disableLabel?: boolean
|
||||
overrides?: Partial<GroupField>
|
||||
}) => Field
|
||||
|
||||
export const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } = {}) => {
|
||||
const linkResult: GroupField = {
|
||||
name: 'link',
|
||||
type: 'group',
|
||||
admin: {
|
||||
hideGutter: true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'type',
|
||||
type: 'radio',
|
||||
admin: {
|
||||
layout: 'horizontal',
|
||||
width: '50%',
|
||||
},
|
||||
defaultValue: 'reference',
|
||||
options: [
|
||||
{
|
||||
label: 'Internal link',
|
||||
value: 'reference',
|
||||
},
|
||||
{
|
||||
label: 'Custom URL',
|
||||
value: 'custom',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'newTab',
|
||||
type: 'checkbox',
|
||||
admin: {
|
||||
style: {
|
||||
alignSelf: 'flex-end',
|
||||
},
|
||||
width: '50%',
|
||||
},
|
||||
label: 'Open in new tab',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const linkTypes: Field[] = [
|
||||
{
|
||||
name: 'reference',
|
||||
type: 'relationship',
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData?.type === 'reference',
|
||||
},
|
||||
label: 'Document to link to',
|
||||
relationTo: ['posts'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'url',
|
||||
type: 'text',
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData?.type === 'custom',
|
||||
},
|
||||
label: 'Custom URL',
|
||||
required: true,
|
||||
},
|
||||
]
|
||||
|
||||
if (!disableLabel) {
|
||||
linkTypes.map((linkType) => ({
|
||||
...linkType,
|
||||
admin: {
|
||||
...linkType.admin,
|
||||
width: '50%',
|
||||
},
|
||||
}))
|
||||
|
||||
linkResult.fields.push({
|
||||
type: 'row',
|
||||
fields: [
|
||||
...linkTypes,
|
||||
{
|
||||
name: 'label',
|
||||
type: 'text',
|
||||
admin: {
|
||||
width: '50%',
|
||||
},
|
||||
label: 'Label',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
} else {
|
||||
linkResult.fields = [...linkResult.fields, ...linkTypes]
|
||||
}
|
||||
|
||||
if (appearances !== false) {
|
||||
let appearanceOptionsToUse = [appearanceOptions.default, appearanceOptions.outline]
|
||||
|
||||
if (appearances) {
|
||||
appearanceOptionsToUse = appearances.map((appearance) => appearanceOptions[appearance])
|
||||
}
|
||||
|
||||
linkResult.fields.push({
|
||||
name: 'appearance',
|
||||
type: 'select',
|
||||
admin: {
|
||||
description: 'Choose how the link should be rendered.',
|
||||
},
|
||||
defaultValue: 'default',
|
||||
options: appearanceOptionsToUse,
|
||||
})
|
||||
}
|
||||
|
||||
return deepMerge(linkResult, overrides)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { ArrayField, Field } from 'payload'
|
||||
|
||||
import deepMerge from '../utilities/deepMerge.ts'
|
||||
import type { LinkAppearances } from './link.ts'
|
||||
import { link } from './link.ts'
|
||||
|
||||
type LinkGroupType = (options?: {
|
||||
appearances?: LinkAppearances[] | false
|
||||
overrides?: Partial<ArrayField>
|
||||
}) => Field
|
||||
|
||||
export const linkGroup: LinkGroupType = ({ appearances, overrides = {} } = {}) => {
|
||||
const generatedLinkGroup: Field = {
|
||||
name: 'links',
|
||||
type: 'array',
|
||||
fields: [
|
||||
link({
|
||||
appearances,
|
||||
}),
|
||||
],
|
||||
admin: {
|
||||
initCollapsed: true,
|
||||
},
|
||||
}
|
||||
|
||||
return deepMerge(generatedLinkGroup, overrides)
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { Button, FieldLabel, TextInput, useField, useForm, useFormFields } from '@payloadcms/ui'
|
||||
import { TextFieldClientProps } from 'payload'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
|
||||
import { formatSlug } from './formatSlug'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
type SlugComponentProps = {
|
||||
fieldToUse: string
|
||||
checkboxFieldPath: string
|
||||
} & TextFieldClientProps
|
||||
|
||||
export const SlugComponent: React.FC<SlugComponentProps> = ({
|
||||
field,
|
||||
fieldToUse,
|
||||
checkboxFieldPath: checkboxFieldPathFromProps,
|
||||
path,
|
||||
readOnly: readOnlyFromProps,
|
||||
}) => {
|
||||
const { label } = field
|
||||
|
||||
const checkboxFieldPath = path?.includes('.')
|
||||
? `${path}.${checkboxFieldPathFromProps}`
|
||||
: checkboxFieldPathFromProps
|
||||
|
||||
const { value, setValue } = useField<string>({ path: path || field.name })
|
||||
|
||||
const { dispatchFields } = useForm()
|
||||
|
||||
// The value of the checkbox
|
||||
// We're using separate useFormFields to minimise re-renders
|
||||
const checkboxValue = useFormFields(([fields]) => {
|
||||
return fields[checkboxFieldPath]?.value as string
|
||||
})
|
||||
|
||||
// The value of the field we're listening to for the slug
|
||||
const targetFieldValue = useFormFields(([fields]) => {
|
||||
return fields[fieldToUse]?.value as string
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (checkboxValue) {
|
||||
if (targetFieldValue) {
|
||||
const formattedSlug = formatSlug(targetFieldValue)
|
||||
|
||||
if (value !== formattedSlug) setValue(formattedSlug)
|
||||
} else {
|
||||
if (value !== '') setValue('')
|
||||
}
|
||||
}
|
||||
}, [targetFieldValue, checkboxValue, setValue, value])
|
||||
|
||||
const handleLock = useCallback(
|
||||
(e: React.MouseEvent<Element>) => {
|
||||
e.preventDefault()
|
||||
|
||||
dispatchFields({
|
||||
type: 'UPDATE',
|
||||
path: checkboxFieldPath,
|
||||
value: !checkboxValue,
|
||||
})
|
||||
},
|
||||
[checkboxValue, checkboxFieldPath, dispatchFields]
|
||||
)
|
||||
|
||||
const readOnly = readOnlyFromProps || checkboxValue
|
||||
|
||||
return (
|
||||
<div className="field-type slug-field-component">
|
||||
<div className="label-wrapper">
|
||||
<FieldLabel htmlFor={`field-${path}`} label={label} />
|
||||
|
||||
<Button className="lock-button" buttonStyle="none" onClick={handleLock}>
|
||||
{checkboxValue ? 'Unlock' : 'Lock'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<TextInput
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
path={path || field.name}
|
||||
readOnly={Boolean(readOnly)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import type { FieldHook } from 'payload'
|
||||
|
||||
export const formatSlug = (val: string): string =>
|
||||
val
|
||||
.replace(/ /g, '-')
|
||||
.replace(/[^\w-]+/g, '')
|
||||
.toLowerCase()
|
||||
|
||||
export const formatSlugHook =
|
||||
(fallback: string): FieldHook =>
|
||||
({ data, operation, value }) => {
|
||||
if (typeof value === 'string') {
|
||||
return formatSlug(value)
|
||||
}
|
||||
|
||||
if (operation === 'create' || !data?.slug) {
|
||||
const fallbackData = data?.[fallback] || data?.[fallback]
|
||||
|
||||
if (fallbackData && typeof fallbackData === 'string') {
|
||||
return formatSlug(fallbackData)
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
.slug-field-component {
|
||||
.label-wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.lock-button {
|
||||
margin: 0;
|
||||
padding-bottom: 0.3125rem;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import type { CheckboxField, TextField } from 'payload'
|
||||
|
||||
import { formatSlugHook } from './formatSlug.ts'
|
||||
|
||||
type Overrides = {
|
||||
slugOverrides?: Partial<TextField>
|
||||
checkboxOverrides?: Partial<CheckboxField>
|
||||
}
|
||||
|
||||
type Slug = (fieldToUse?: string, overrides?: Overrides) => [TextField, CheckboxField]
|
||||
|
||||
export const slugField: Slug = (fieldToUse = 'title', overrides = {}) => {
|
||||
const { slugOverrides, checkboxOverrides } = overrides
|
||||
|
||||
const checkBoxField: CheckboxField = {
|
||||
name: 'slugLock',
|
||||
type: 'checkbox',
|
||||
defaultValue: true,
|
||||
admin: {
|
||||
hidden: true,
|
||||
position: 'sidebar',
|
||||
},
|
||||
...checkboxOverrides,
|
||||
}
|
||||
|
||||
// @ts-expect-error - ts mismatch Partial<TextField> with TextField
|
||||
const slugField: TextField = {
|
||||
name: 'slug',
|
||||
type: 'text',
|
||||
index: true,
|
||||
label: 'Slug',
|
||||
...(slugOverrides || {}),
|
||||
hooks: {
|
||||
// Kept this in for hook or API based updates
|
||||
beforeValidate: [formatSlugHook(fieldToUse)],
|
||||
},
|
||||
admin: {
|
||||
// position: 'sidebar',
|
||||
...(slugOverrides?.admin || {}),
|
||||
components: {
|
||||
Field: {
|
||||
path: '@/fields/slug/SlugComponent#SlugComponent',
|
||||
clientProps: {
|
||||
fieldToUse,
|
||||
checkboxFieldPath: checkBoxField.name,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return [slugField, checkBoxField]
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { FieldHook } from 'payload'
|
||||
|
||||
const format = (val: string): string =>
|
||||
val
|
||||
.replace(/ /g, '-')
|
||||
.replace(/[^\w-]+/g, '')
|
||||
.toLowerCase()
|
||||
|
||||
const formatSlug =
|
||||
(fallback: string): FieldHook =>
|
||||
({ data, operation, originalDoc, value }) => {
|
||||
if (typeof value === 'string') {
|
||||
return format(value)
|
||||
}
|
||||
|
||||
if (operation === 'create') {
|
||||
const fallbackData = data?.[fallback] || originalDoc?.[fallback]
|
||||
|
||||
if (fallbackData && typeof fallbackData === 'string') {
|
||||
return format(fallbackData)
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
export default formatSlug
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { CollectionBeforeChangeHook } from 'payload'
|
||||
|
||||
export const populatePublishedAt: CollectionBeforeChangeHook = ({ data, operation, req }) => {
|
||||
if (operation === 'create' || operation === 'update') {
|
||||
if (req.data && !req.data.publishedAt) {
|
||||
const now = new Date()
|
||||
return {
|
||||
...data,
|
||||
publishedAt: now,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { revalidateTag } from 'next/cache'
|
||||
import type { CollectionAfterChangeHook } from 'payload'
|
||||
|
||||
export const revalidateRedirects: CollectionAfterChangeHook = ({ doc, req: { payload } }) => {
|
||||
payload.logger.info(`Revalidating redirects`)
|
||||
|
||||
revalidateTag('redirects')
|
||||
|
||||
return doc
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1,15 +0,0 @@
|
||||
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
||||
|
||||
export async function up({ db }: MigrateUpArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
INSERT INTO storage.buckets (id, name, public) VALUES ('cms', 'cms', false)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
`)
|
||||
}
|
||||
|
||||
export async function down({ db }: MigrateDownArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
DELETE FROM storage.objects WHERE bucket_id = 'cms';
|
||||
DELETE FROM storage.buckets WHERE id = 'cms';
|
||||
`)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,21 +0,0 @@
|
||||
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
||||
|
||||
export async function up({ db }: MigrateUpArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
CREATE TABLE "users_sessions" (
|
||||
"_order" integer NOT NULL,
|
||||
"_parent_id" integer NOT NULL,
|
||||
"id" varchar PRIMARY KEY NOT NULL,
|
||||
"created_at" timestamp(3) with time zone,
|
||||
"expires_at" timestamp(3) with time zone NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE "users_sessions" ADD CONSTRAINT "users_sessions_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
CREATE INDEX "users_sessions_order_idx" ON "users_sessions" USING btree ("_order");
|
||||
CREATE INDEX "users_sessions_parent_id_idx" ON "users_sessions" USING btree ("_parent_id");`)
|
||||
}
|
||||
|
||||
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
DROP TABLE "users_sessions" CASCADE;`)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,15 +0,0 @@
|
||||
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
||||
|
||||
export async function up({ db }: MigrateUpArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "authors" ALTER COLUMN "author_id" SET NOT NULL;
|
||||
ALTER TABLE "authors" ADD COLUMN "company" varchar;
|
||||
CREATE UNIQUE INDEX "authors_author_id_idx" ON "authors" USING btree ("author_id");`)
|
||||
}
|
||||
|
||||
export async function down({ db }: MigrateDownArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
DROP INDEX "authors_author_id_idx";
|
||||
ALTER TABLE "authors" ALTER COLUMN "author_id" DROP NOT NULL;
|
||||
ALTER TABLE "authors" DROP COLUMN "company";`)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,23 +0,0 @@
|
||||
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
||||
|
||||
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "posts" ALTER COLUMN "toc_depth" SET DEFAULT 3;
|
||||
ALTER TABLE "_posts_v" ALTER COLUMN "version_toc_depth" SET DEFAULT 3;
|
||||
CREATE INDEX "authors_author_idx" ON "authors" USING btree ("author");
|
||||
CREATE INDEX "categories_name_idx" ON "categories" USING btree ("name");
|
||||
CREATE INDEX "posts_title_idx" ON "posts" USING btree ("title");
|
||||
CREATE INDEX "_posts_v_version_version_title_idx" ON "_posts_v" USING btree ("version_title");
|
||||
CREATE INDEX "tags_name_idx" ON "tags" USING btree ("name");`)
|
||||
}
|
||||
|
||||
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
DROP INDEX "authors_author_idx";
|
||||
DROP INDEX "categories_name_idx";
|
||||
DROP INDEX "posts_title_idx";
|
||||
DROP INDEX "_posts_v_version_version_title_idx";
|
||||
DROP INDEX "tags_name_idx";
|
||||
ALTER TABLE "posts" ALTER COLUMN "toc_depth" SET DEFAULT 2;
|
||||
ALTER TABLE "_posts_v" ALTER COLUMN "version_toc_depth" SET DEFAULT 2;`)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,21 +0,0 @@
|
||||
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
||||
|
||||
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
DROP INDEX "_customers_v_autosave_idx";
|
||||
DROP INDEX "_events_v_autosave_idx";
|
||||
DROP INDEX "_posts_v_autosave_idx";
|
||||
ALTER TABLE "_customers_v" DROP COLUMN "autosave";
|
||||
ALTER TABLE "_events_v" DROP COLUMN "autosave";
|
||||
ALTER TABLE "_posts_v" DROP COLUMN "autosave";`)
|
||||
}
|
||||
|
||||
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "_customers_v" ADD COLUMN "autosave" boolean;
|
||||
ALTER TABLE "_events_v" ADD COLUMN "autosave" boolean;
|
||||
ALTER TABLE "_posts_v" ADD COLUMN "autosave" boolean;
|
||||
CREATE INDEX "_customers_v_autosave_idx" ON "_customers_v" USING btree ("autosave");
|
||||
CREATE INDEX "_events_v_autosave_idx" ON "_events_v" USING btree ("autosave");
|
||||
CREATE INDEX "_posts_v_autosave_idx" ON "_posts_v" USING btree ("autosave");`)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Loaded 100 of 215 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user