Merge branch 'master' of github.com:supabase/supabase into self-hosted/envoy-test

This commit is contained in:
Andrey A. committed 2026-02-11 18:53:42 +01:00
commit 7d27b322c5
215 files changed
+3152 -64030

No files matched your search

-2
View File
@@ -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
-5
View File
@@ -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
-15
View File
@@ -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
-42
View File
@@ -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
-48
View File
@@ -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
-71
View File
@@ -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
-23
View File
@@ -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
-30
View File
@@ -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:
-4
View File
@@ -1,4 +0,0 @@
const { defineConfig } = require('eslint/config')
const supabaseConfig = require('eslint-config-supabase/next')
module.exports = defineConfig([supabaseConfig])
-54
View File
@@ -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 })
-64
View File
@@ -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"
}
}
-29
View File
@@ -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()
-15
View File
@@ -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'))
}
-21
View File
@@ -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
}
-3
View File
@@ -1,3 +0,0 @@
import type { Access } from 'payload'
export const isAnyone: Access = () => true
-9
View File
@@ -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)
}
-19
View File
@@ -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>
)
}
-5
View File
@@ -1,5 +0,0 @@
import { redirect } from 'next/navigation'
export default async function HomePage() {
redirect('/admin')
}
-164
View File
@@ -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.
-31
View File
@@ -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
-14
View File
@@ -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)
}
-26
View File
@@ -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>
)
}
-36
View File
@@ -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>
)
}
-21
View File
@@ -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>
)
}
-35
View File
@@ -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>
)
}
-57
View File
@@ -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>
)
}
-14
View File
@@ -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,
},
],
}
-16
View File
@@ -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>
`
}
-27
View File
@@ -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',
}
-22
View File
@@ -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>
)
}
-18
View File
@@ -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',
}
-70
View File
@@ -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
-27
View File
@@ -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
}
-317
View File
@@ -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
}
-407
View File
@@ -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
-71
View File
@@ -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
}
-285
View File
@@ -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
-22
View File
@@ -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
-47
View File
@@ -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&apos;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
-88
View File
@@ -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>, &nbsp;</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>
)
}
-64
View File
@@ -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()} />
}
-29
View File
@@ -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
}
-24
View File
@@ -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>
)
}
-22
View File
@@ -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}
/>
)
}
-53
View File
@@ -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 }
-49
View File
@@ -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 }
-28
View File
@@ -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 }
-23
View File
@@ -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 }
-20
View File
@@ -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 }
-92
View File
@@ -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,
}
-145
View File
@@ -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,
}
-22
View File
@@ -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 }
-12
View File
@@ -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,
},
}
-14
View File
@@ -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 {}
-47
View File
@@ -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,
},
]
},
}),
],
})
-139
View File
@@ -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)
}
-27
View File
@@ -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>
)
}
-25
View File
@@ -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
}
-12
View File
@@ -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;
}
}
-53
View File
@@ -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]
}
-27
View File
@@ -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
-15
View File
@@ -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
}
-10
View File
@@ -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