mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 05:39:11 +03:00
Merge branch 'master' into feat/local-snippets
This commit is contained in:
commit
f1b226e58d
320 files changed
+29690
-12893
No files matched your search
@@ -0,0 +1,17 @@
|
||||
# [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
|
||||
|
||||
BLOG_APP_URL=http://localhost:3000
|
||||
NEXT_PUBLIC_SERVER_URL=http://localhost:3000/blog
|
||||
CRON_SECRET=secret
|
||||
PREVIEW_SECRET=secret
|
||||
@@ -1,13 +0,0 @@
|
||||
DATABASE_URI=postgres://postgres:<password>@127.0.0.1:5432/your-database-name
|
||||
PAYLOAD_SECRET=YOUR_SECRET_HERE
|
||||
|
||||
S3_BUCKET=
|
||||
S3_ACCESS_KEY_ID=
|
||||
S3_SECRET_ACCESS_KEY=
|
||||
S3_REGION=
|
||||
S3_ENDPOINT=
|
||||
|
||||
NEXT_PUBLIC_SERVER_URL=http://localhost:3000/blog
|
||||
CRON_SECRET=YOUR_CRON_SECRET_HERE
|
||||
PREVIEW_SECRET=YOUR_SECRET_HERE
|
||||
BLOG_APP_URL=http://localhost:3000
|
||||
File renamed without changes.
+2
-3
@@ -29,7 +29,8 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
!.env
|
||||
.env.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
@@ -38,6 +39,4 @@ yarn-error.log*
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
.env
|
||||
|
||||
/media
|
||||
@@ -0,0 +1,48 @@
|
||||
# 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
|
||||
@@ -15,7 +15,7 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
# Uncomment the following to use postgres
|
||||
# Uncomment the following to use postgres
|
||||
postgres:
|
||||
restart: always
|
||||
image: postgres:latest
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
import { withPayload } from '@payloadcms/next/withPayload'
|
||||
|
||||
import redirects from './redirects.js'
|
||||
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 NEXT_PUBLIC_SERVER_URL = process.env.VERCEL_PROJECT_PRODUCTION_URL
|
||||
? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
|
||||
|
||||
+32
-16
@@ -5,43 +5,59 @@
|
||||
"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": "cross-env NODE_OPTIONS=--no-deprecation next lint",
|
||||
"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",
|
||||
"ci": "cross-env NODE_OPTIONS=--no-deprecation payload migrate && pnpm build",
|
||||
"clean": "rimraf node_modules .next",
|
||||
"start": "cross-env NODE_OPTIONS=--no-deprecation next start --port 3030",
|
||||
"vercel-build": "pnpm migrate && pnpm build",
|
||||
"typecheck_IGNORED": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/db-postgres": "3.33.0",
|
||||
"@payloadcms/live-preview-react": "^3.33.0",
|
||||
"@payloadcms/next": "3.33.0",
|
||||
"@payloadcms/payload-cloud": "3.33.0",
|
||||
"@payloadcms/plugin-form-builder": "3.33.0",
|
||||
"@payloadcms/plugin-nested-docs": "3.33.0",
|
||||
"@payloadcms/plugin-seo": "3.33.0",
|
||||
"@payloadcms/richtext-lexical": "3.33.0",
|
||||
"@payloadcms/storage-s3": "3.33.0",
|
||||
"@payloadcms/ui": "3.33.0",
|
||||
"@payloadcms/admin-bar": "^3.50.0",
|
||||
"@payloadcms/db-postgres": "^3.50.0",
|
||||
"@payloadcms/live-preview-react": "^3.50.0",
|
||||
"@payloadcms/next": "3.50.0",
|
||||
"@payloadcms/payload-cloud": "3.50.0",
|
||||
"@payloadcms/plugin-form-builder": "3.50.0",
|
||||
"@payloadcms/plugin-nested-docs": "3.50.0",
|
||||
"@payloadcms/plugin-seo": "3.50.0",
|
||||
"@payloadcms/richtext-lexical": "3.50.0",
|
||||
"@payloadcms/storage-s3": "3.50.0",
|
||||
"@payloadcms/ui": "3.50.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",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^1.2.1",
|
||||
"common": "workspace:*",
|
||||
"config": "workspace:*",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint-config-supabase": "workspace:*",
|
||||
"graphql": "^16.8.1",
|
||||
"graphql": "^16.11.0",
|
||||
"image-size": "2.0.2",
|
||||
"lucide-react": "^0.511.0",
|
||||
"next": "catalog:",
|
||||
"payload": "3.33.0",
|
||||
"payload": "3.50.0",
|
||||
"pg": "^8.16.3",
|
||||
"prism-react-renderer": "^2.3.1",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"sharp": "0.32.6"
|
||||
"sharp": "0.32.6",
|
||||
"tailwind-merge": "^1.13.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "5.7.3"
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
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
|
||||
}
|
||||
|
||||
export default redirects
|
||||
@@ -0,0 +1,28 @@
|
||||
import payload from 'payload'
|
||||
// Use a minimal config for migrations to avoid importing the full app graph during build
|
||||
import payloadConfig from '../src/payload.migrate.config.ts'
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
await payload.init({ config: payloadConfig })
|
||||
|
||||
// Ensure non-interactive: remove any dev-mode migration sentinel rows
|
||||
try {
|
||||
await payload.delete({
|
||||
collection: 'payload-migrations',
|
||||
where: { batch: { equals: -1 } },
|
||||
})
|
||||
} catch {}
|
||||
|
||||
await payload.db.migrate()
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('✅ Payload migrations complete')
|
||||
process.exit(0)
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('❌ Payload migrations failed:', err)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AccessArgs, FieldAccess } from 'payload'
|
||||
import type { User } from '@/payload-types'
|
||||
import type { User } from '../payload-types'
|
||||
|
||||
type isAdmin = (args: AccessArgs<User>) => boolean
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AccessArgs } from 'payload'
|
||||
|
||||
import type { User } from '@/payload-types'
|
||||
import type { User } from '../payload-types'
|
||||
|
||||
type isAuthenticated = (args: AccessArgs<User>) => boolean
|
||||
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import type { Post, ArchiveBlock as ArchiveBlockProps } from '@/payload-types'
|
||||
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
import React from 'react'
|
||||
import RichText from '@/components/RichText'
|
||||
|
||||
import { CollectionArchive } from '@/components/CollectionArchive'
|
||||
|
||||
export const ArchiveBlock: React.FC<
|
||||
ArchiveBlockProps & {
|
||||
id?: string
|
||||
}
|
||||
> = async (props) => {
|
||||
const { id, categories, introContent, limit: limitFromProps, populateBy, selectedDocs } = props
|
||||
|
||||
const limit = limitFromProps || 3
|
||||
|
||||
let posts: Post[] = []
|
||||
|
||||
if (populateBy === 'collection') {
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
const flattenedCategories = categories?.map((category) => {
|
||||
if (typeof category === 'object') return category.id
|
||||
else return category
|
||||
})
|
||||
|
||||
const fetchedPosts = await payload.find({
|
||||
collection: 'posts',
|
||||
depth: 1,
|
||||
limit,
|
||||
...(flattenedCategories && flattenedCategories.length > 0
|
||||
? {
|
||||
where: {
|
||||
categories: {
|
||||
in: flattenedCategories,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
|
||||
posts = fetchedPosts.docs
|
||||
} else {
|
||||
if (selectedDocs?.length) {
|
||||
const filteredSelectedPosts = selectedDocs.map((post) => {
|
||||
if (typeof post.value === 'object') return post.value
|
||||
}) as Post[]
|
||||
|
||||
posts = filteredSelectedPosts
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="my-16" id={`block-${id}`}>
|
||||
{introContent && (
|
||||
<div className="container mb-16">
|
||||
<RichText className="ms-0 max-w-[48rem]" data={introContent} enableGutter={false} />
|
||||
</div>
|
||||
)}
|
||||
<CollectionArchive posts={posts} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import type { Block } from 'payload'
|
||||
|
||||
import {
|
||||
FixedToolbarFeature,
|
||||
HeadingFeature,
|
||||
InlineToolbarFeature,
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
export const Archive: Block = {
|
||||
slug: 'archive',
|
||||
interfaceName: 'ArchiveBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'introContent',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ rootFeatures }) => {
|
||||
return [
|
||||
...rootFeatures,
|
||||
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
|
||||
FixedToolbarFeature(),
|
||||
InlineToolbarFeature(),
|
||||
]
|
||||
},
|
||||
}),
|
||||
label: 'Intro Content',
|
||||
},
|
||||
{
|
||||
name: 'populateBy',
|
||||
type: 'select',
|
||||
defaultValue: 'collection',
|
||||
options: [
|
||||
{
|
||||
label: 'Collection',
|
||||
value: 'collection',
|
||||
},
|
||||
{
|
||||
label: 'Individual Selection',
|
||||
value: 'selection',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'relationTo',
|
||||
type: 'select',
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData.populateBy === 'collection',
|
||||
},
|
||||
defaultValue: 'posts',
|
||||
label: 'Collections To Show',
|
||||
options: [
|
||||
{
|
||||
label: 'Posts',
|
||||
value: 'posts',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'categories',
|
||||
type: 'relationship',
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData.populateBy === 'collection',
|
||||
},
|
||||
hasMany: true,
|
||||
label: 'Categories To Show',
|
||||
relationTo: 'categories',
|
||||
},
|
||||
{
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData.populateBy === 'collection',
|
||||
step: 1,
|
||||
},
|
||||
defaultValue: 10,
|
||||
label: 'Limit',
|
||||
},
|
||||
{
|
||||
name: 'selectedDocs',
|
||||
type: 'relationship',
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData.populateBy === 'selection',
|
||||
},
|
||||
hasMany: true,
|
||||
label: 'Selection',
|
||||
relationTo: ['posts'],
|
||||
},
|
||||
],
|
||||
labels: {
|
||||
plural: 'Archives',
|
||||
singular: 'Archive',
|
||||
},
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import type { CallToActionBlock as CTABlockProps } from '@/payload-types'
|
||||
|
||||
import RichText from '@/components/RichText'
|
||||
import { CMSLink } from '@/components/Link'
|
||||
|
||||
export const CallToActionBlock: React.FC<CTABlockProps> = ({ links, richText }) => {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="bg-card rounded border-border border p-4 flex flex-col gap-8 md:flex-row md:justify-between md:items-center">
|
||||
<div className="max-w-[48rem] flex items-center">
|
||||
{richText && <RichText className="mb-0" data={richText} enableGutter={false} />}
|
||||
</div>
|
||||
<div className="flex flex-col gap-8">
|
||||
{(links || []).map(({ link }, i) => {
|
||||
return <CMSLink key={i} size="lg" {...link} />
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import type { Block } from 'payload'
|
||||
|
||||
import {
|
||||
FixedToolbarFeature,
|
||||
HeadingFeature,
|
||||
InlineToolbarFeature,
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { linkGroup } from '../../fields/linkGroup'
|
||||
|
||||
export const CallToAction: Block = {
|
||||
slug: 'cta',
|
||||
interfaceName: 'CallToActionBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'richText',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ rootFeatures }) => {
|
||||
return [
|
||||
...rootFeatures,
|
||||
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
|
||||
FixedToolbarFeature(),
|
||||
InlineToolbarFeature(),
|
||||
]
|
||||
},
|
||||
}),
|
||||
label: false,
|
||||
},
|
||||
linkGroup({
|
||||
appearances: ['default', 'outline'],
|
||||
overrides: {
|
||||
maxRows: 2,
|
||||
},
|
||||
}),
|
||||
],
|
||||
labels: {
|
||||
plural: 'Calls to Action',
|
||||
singular: 'Call to Action',
|
||||
},
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export function CopyButton({ code }: { code: string }) {
|
||||
<div className="flex justify-end align-middle">
|
||||
<Button
|
||||
className="flex gap-1"
|
||||
variant={'secondary'}
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(code)
|
||||
updateCopyStatus()
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { cn } from '@/utilities/ui'
|
||||
import React from 'react'
|
||||
import RichText from '@/components/RichText'
|
||||
|
||||
import type { ContentBlock as ContentBlockProps } from '@/payload-types'
|
||||
|
||||
import { CMSLink } from '../../components/Link'
|
||||
|
||||
export const ContentBlock: React.FC<ContentBlockProps> = (props) => {
|
||||
const { columns } = props
|
||||
|
||||
const colsSpanClasses = {
|
||||
full: '12',
|
||||
half: '6',
|
||||
oneThird: '4',
|
||||
twoThirds: '8',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container my-16">
|
||||
<div className="grid grid-cols-4 lg:grid-cols-12 gap-y-8 gap-x-16">
|
||||
{columns &&
|
||||
columns.length > 0 &&
|
||||
columns.map((col, index) => {
|
||||
const { enableLink, link, richText, size } = col
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(`col-span-4 lg:col-span-${colsSpanClasses[size!]}`, {
|
||||
'md:col-span-2': size !== 'full',
|
||||
})}
|
||||
key={index}
|
||||
>
|
||||
{richText && <RichText data={richText} enableGutter={false} />}
|
||||
|
||||
{enableLink && <CMSLink {...link} />}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import type { Block, Field } from 'payload'
|
||||
|
||||
import {
|
||||
FixedToolbarFeature,
|
||||
HeadingFeature,
|
||||
InlineToolbarFeature,
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { link } from '@/fields/link'
|
||||
|
||||
const columnFields: Field[] = [
|
||||
{
|
||||
name: 'size',
|
||||
type: 'select',
|
||||
defaultValue: 'oneThird',
|
||||
options: [
|
||||
{
|
||||
label: 'One Third',
|
||||
value: 'oneThird',
|
||||
},
|
||||
{
|
||||
label: 'Half',
|
||||
value: 'half',
|
||||
},
|
||||
{
|
||||
label: 'Two Thirds',
|
||||
value: 'twoThirds',
|
||||
},
|
||||
{
|
||||
label: 'Full',
|
||||
value: 'full',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'richText',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ rootFeatures }) => {
|
||||
return [
|
||||
...rootFeatures,
|
||||
HeadingFeature({ enabledHeadingSizes: ['h2', 'h3', 'h4'] }),
|
||||
FixedToolbarFeature(),
|
||||
InlineToolbarFeature(),
|
||||
]
|
||||
},
|
||||
}),
|
||||
label: false,
|
||||
},
|
||||
{
|
||||
name: 'enableLink',
|
||||
type: 'checkbox',
|
||||
},
|
||||
link({
|
||||
overrides: {
|
||||
admin: {
|
||||
condition: (_data, siblingData) => {
|
||||
return Boolean(siblingData?.enableLink)
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
||||
export const Content: Block = {
|
||||
slug: 'content',
|
||||
interfaceName: 'ContentBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'columns',
|
||||
type: 'array',
|
||||
admin: {
|
||||
initCollapsed: true,
|
||||
},
|
||||
fields: columnFields,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import type { CheckboxField } from '@payloadcms/plugin-form-builder/types'
|
||||
import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form'
|
||||
|
||||
import { useFormContext } from 'react-hook-form'
|
||||
|
||||
import { Checkbox as CheckboxUi } from '@/components/ui/checkbox'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import React from 'react'
|
||||
|
||||
import { Error } from '../Error'
|
||||
import { Width } from '../Width'
|
||||
|
||||
export const Checkbox: React.FC<
|
||||
CheckboxField & {
|
||||
errors: Partial<FieldErrorsImpl>
|
||||
register: UseFormRegister<FieldValues>
|
||||
}
|
||||
> = ({ name, defaultValue, errors, label, register, required, width }) => {
|
||||
const props = register(name, { required: required })
|
||||
const { setValue } = useFormContext()
|
||||
|
||||
return (
|
||||
<Width width={width}>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckboxUi
|
||||
defaultChecked={defaultValue}
|
||||
id={name}
|
||||
{...props}
|
||||
onCheckedChange={(checked) => {
|
||||
setValue(props.name, checked)
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor={name}>
|
||||
{required && (
|
||||
<span className="required">
|
||||
* <span className="sr-only">(required)</span>
|
||||
</span>
|
||||
)}
|
||||
{label}
|
||||
</Label>
|
||||
</div>
|
||||
{errors[name] && <Error name={name} />}
|
||||
</Width>
|
||||
)
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
'use client'
|
||||
import type { FormFieldBlock, Form as FormType } from '@payloadcms/plugin-form-builder/types'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useForm, FormProvider } from 'react-hook-form'
|
||||
import RichText from '@/components/RichText'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
import { fields } from './fields'
|
||||
import { getClientSideURL } from '@/utilities/getURL'
|
||||
|
||||
export type FormBlockType = {
|
||||
blockName?: string
|
||||
blockType?: 'formBlock'
|
||||
enableIntro: boolean
|
||||
form: FormType
|
||||
introContent?: SerializedEditorState
|
||||
}
|
||||
|
||||
export const FormBlock: React.FC<
|
||||
{
|
||||
id?: string
|
||||
} & FormBlockType
|
||||
> = (props) => {
|
||||
const {
|
||||
enableIntro,
|
||||
form: formFromProps,
|
||||
form: { id: formID, confirmationMessage, confirmationType, redirect, submitButtonLabel } = {},
|
||||
introContent,
|
||||
} = props
|
||||
|
||||
const formMethods = useForm({
|
||||
defaultValues: formFromProps.fields,
|
||||
})
|
||||
const {
|
||||
control,
|
||||
formState: { errors },
|
||||
handleSubmit,
|
||||
register,
|
||||
} = formMethods
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [hasSubmitted, setHasSubmitted] = useState<boolean>()
|
||||
const [error, setError] = useState<{ message: string; status?: string } | undefined>()
|
||||
const router = useRouter()
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(data: FormFieldBlock[]) => {
|
||||
let loadingTimerID: ReturnType<typeof setTimeout>
|
||||
const submitForm = async () => {
|
||||
setError(undefined)
|
||||
|
||||
const dataToSend = Object.entries(data).map(([name, value]) => ({
|
||||
field: name,
|
||||
value,
|
||||
}))
|
||||
|
||||
// delay loading indicator by 1s
|
||||
loadingTimerID = setTimeout(() => {
|
||||
setIsLoading(true)
|
||||
}, 1000)
|
||||
|
||||
try {
|
||||
const req = await fetch(`${getClientSideURL()}/api/form-submissions`, {
|
||||
body: JSON.stringify({
|
||||
form: formID,
|
||||
submissionData: dataToSend,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
const res = await req.json()
|
||||
|
||||
clearTimeout(loadingTimerID)
|
||||
|
||||
if (req.status >= 400) {
|
||||
setIsLoading(false)
|
||||
|
||||
setError({
|
||||
message: res.errors?.[0]?.message || 'Internal Server Error',
|
||||
status: res.status,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
setHasSubmitted(true)
|
||||
|
||||
if (confirmationType === 'redirect' && redirect) {
|
||||
const { url } = redirect
|
||||
|
||||
const redirectUrl = url
|
||||
|
||||
if (redirectUrl) router.push(redirectUrl)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
setIsLoading(false)
|
||||
setError({
|
||||
message: 'Something went wrong.',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
void submitForm()
|
||||
},
|
||||
[router, formID, redirect, confirmationType]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="container lg:max-w-[48rem]">
|
||||
{enableIntro && introContent && !hasSubmitted && (
|
||||
<RichText className="mb-8 lg:mb-12" data={introContent} enableGutter={false} />
|
||||
)}
|
||||
<div className="p-4 lg:p-6 border border-border rounded-[0.8rem]">
|
||||
<FormProvider {...formMethods}>
|
||||
{!isLoading && hasSubmitted && confirmationType === 'message' && (
|
||||
<RichText data={confirmationMessage} />
|
||||
)}
|
||||
{isLoading && !hasSubmitted && <p>Loading, please wait...</p>}
|
||||
{error && <div>{`${error.status || '500'}: ${error.message || ''}`}</div>}
|
||||
{!hasSubmitted && (
|
||||
<form id={formID} onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="mb-4 last:mb-0">
|
||||
{formFromProps &&
|
||||
formFromProps.fields &&
|
||||
formFromProps.fields?.map((field, index) => {
|
||||
const Field: React.FC<any> = fields?.[field.blockType as keyof typeof fields]
|
||||
if (Field) {
|
||||
return (
|
||||
<div className="mb-6 last:mb-0" key={index}>
|
||||
<Field
|
||||
form={formFromProps}
|
||||
{...field}
|
||||
{...formMethods}
|
||||
control={control}
|
||||
errors={errors}
|
||||
register={register}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Button form={formID} type="submit" variant="default">
|
||||
{submitButtonLabel}
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import type { CountryField } from '@payloadcms/plugin-form-builder/types'
|
||||
import type { Control, FieldErrorsImpl } from 'react-hook-form'
|
||||
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import React from 'react'
|
||||
import { Controller } from 'react-hook-form'
|
||||
|
||||
import { Error } from '../Error'
|
||||
import { Width } from '../Width'
|
||||
import { countryOptions } from './options'
|
||||
|
||||
export const Country: React.FC<
|
||||
CountryField & {
|
||||
control: Control
|
||||
errors: Partial<FieldErrorsImpl>
|
||||
}
|
||||
> = ({ name, control, errors, label, required, width }) => {
|
||||
return (
|
||||
<Width width={width}>
|
||||
<Label className="" htmlFor={name}>
|
||||
{label}
|
||||
|
||||
{required && (
|
||||
<span className="required">
|
||||
* <span className="sr-only">(required)</span>
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name={name}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const controlledValue = countryOptions.find((t) => t.value === value)
|
||||
|
||||
return (
|
||||
<Select onValueChange={(val) => onChange(val)} value={controlledValue?.value}>
|
||||
<SelectTrigger className="w-full" id={name}>
|
||||
<SelectValue placeholder={label} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{countryOptions.map(({ label, value }) => {
|
||||
return (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
)
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
}}
|
||||
rules={{ required }}
|
||||
/>
|
||||
{errors[name] && <Error name={name} />}
|
||||
</Width>
|
||||
)
|
||||
}
|
||||
@@ -1,982 +0,0 @@
|
||||
export const countryOptions = [
|
||||
{
|
||||
label: 'Afghanistan',
|
||||
value: 'AF',
|
||||
},
|
||||
{
|
||||
label: 'Åland Islands',
|
||||
value: 'AX',
|
||||
},
|
||||
{
|
||||
label: 'Albania',
|
||||
value: 'AL',
|
||||
},
|
||||
{
|
||||
label: 'Algeria',
|
||||
value: 'DZ',
|
||||
},
|
||||
{
|
||||
label: 'American Samoa',
|
||||
value: 'AS',
|
||||
},
|
||||
{
|
||||
label: 'Andorra',
|
||||
value: 'AD',
|
||||
},
|
||||
{
|
||||
label: 'Angola',
|
||||
value: 'AO',
|
||||
},
|
||||
{
|
||||
label: 'Anguilla',
|
||||
value: 'AI',
|
||||
},
|
||||
{
|
||||
label: 'Antarctica',
|
||||
value: 'AQ',
|
||||
},
|
||||
{
|
||||
label: 'Antigua and Barbuda',
|
||||
value: 'AG',
|
||||
},
|
||||
{
|
||||
label: 'Argentina',
|
||||
value: 'AR',
|
||||
},
|
||||
{
|
||||
label: 'Armenia',
|
||||
value: 'AM',
|
||||
},
|
||||
{
|
||||
label: 'Aruba',
|
||||
value: 'AW',
|
||||
},
|
||||
{
|
||||
label: 'Australia',
|
||||
value: 'AU',
|
||||
},
|
||||
{
|
||||
label: 'Austria',
|
||||
value: 'AT',
|
||||
},
|
||||
{
|
||||
label: 'Azerbaijan',
|
||||
value: 'AZ',
|
||||
},
|
||||
{
|
||||
label: 'Bahamas',
|
||||
value: 'BS',
|
||||
},
|
||||
{
|
||||
label: 'Bahrain',
|
||||
value: 'BH',
|
||||
},
|
||||
{
|
||||
label: 'Bangladesh',
|
||||
value: 'BD',
|
||||
},
|
||||
{
|
||||
label: 'Barbados',
|
||||
value: 'BB',
|
||||
},
|
||||
{
|
||||
label: 'Belarus',
|
||||
value: 'BY',
|
||||
},
|
||||
{
|
||||
label: 'Belgium',
|
||||
value: 'BE',
|
||||
},
|
||||
{
|
||||
label: 'Belize',
|
||||
value: 'BZ',
|
||||
},
|
||||
{
|
||||
label: 'Benin',
|
||||
value: 'BJ',
|
||||
},
|
||||
{
|
||||
label: 'Bermuda',
|
||||
value: 'BM',
|
||||
},
|
||||
{
|
||||
label: 'Bhutan',
|
||||
value: 'BT',
|
||||
},
|
||||
{
|
||||
label: 'Bolivia',
|
||||
value: 'BO',
|
||||
},
|
||||
{
|
||||
label: 'Bosnia and Herzegovina',
|
||||
value: 'BA',
|
||||
},
|
||||
{
|
||||
label: 'Botswana',
|
||||
value: 'BW',
|
||||
},
|
||||
{
|
||||
label: 'Bouvet Island',
|
||||
value: 'BV',
|
||||
},
|
||||
{
|
||||
label: 'Brazil',
|
||||
value: 'BR',
|
||||
},
|
||||
{
|
||||
label: 'British Indian Ocean Territory',
|
||||
value: 'IO',
|
||||
},
|
||||
{
|
||||
label: 'Brunei Darussalam',
|
||||
value: 'BN',
|
||||
},
|
||||
{
|
||||
label: 'Bulgaria',
|
||||
value: 'BG',
|
||||
},
|
||||
{
|
||||
label: 'Burkina Faso',
|
||||
value: 'BF',
|
||||
},
|
||||
{
|
||||
label: 'Burundi',
|
||||
value: 'BI',
|
||||
},
|
||||
{
|
||||
label: 'Cambodia',
|
||||
value: 'KH',
|
||||
},
|
||||
{
|
||||
label: 'Cameroon',
|
||||
value: 'CM',
|
||||
},
|
||||
{
|
||||
label: 'Canada',
|
||||
value: 'CA',
|
||||
},
|
||||
{
|
||||
label: 'Cape Verde',
|
||||
value: 'CV',
|
||||
},
|
||||
{
|
||||
label: 'Cayman Islands',
|
||||
value: 'KY',
|
||||
},
|
||||
{
|
||||
label: 'Central African Republic',
|
||||
value: 'CF',
|
||||
},
|
||||
{
|
||||
label: 'Chad',
|
||||
value: 'TD',
|
||||
},
|
||||
{
|
||||
label: 'Chile',
|
||||
value: 'CL',
|
||||
},
|
||||
{
|
||||
label: 'China',
|
||||
value: 'CN',
|
||||
},
|
||||
{
|
||||
label: 'Christmas Island',
|
||||
value: 'CX',
|
||||
},
|
||||
{
|
||||
label: 'Cocos (Keeling) Islands',
|
||||
value: 'CC',
|
||||
},
|
||||
{
|
||||
label: 'Colombia',
|
||||
value: 'CO',
|
||||
},
|
||||
{
|
||||
label: 'Comoros',
|
||||
value: 'KM',
|
||||
},
|
||||
{
|
||||
label: 'Congo',
|
||||
value: 'CG',
|
||||
},
|
||||
{
|
||||
label: 'Congo, The Democratic Republic of the',
|
||||
value: 'CD',
|
||||
},
|
||||
{
|
||||
label: 'Cook Islands',
|
||||
value: 'CK',
|
||||
},
|
||||
{
|
||||
label: 'Costa Rica',
|
||||
value: 'CR',
|
||||
},
|
||||
{
|
||||
label: "Cote D'Ivoire",
|
||||
value: 'CI',
|
||||
},
|
||||
{
|
||||
label: 'Croatia',
|
||||
value: 'HR',
|
||||
},
|
||||
{
|
||||
label: 'Cuba',
|
||||
value: 'CU',
|
||||
},
|
||||
{
|
||||
label: 'Cyprus',
|
||||
value: 'CY',
|
||||
},
|
||||
{
|
||||
label: 'Czech Republic',
|
||||
value: 'CZ',
|
||||
},
|
||||
{
|
||||
label: 'Denmark',
|
||||
value: 'DK',
|
||||
},
|
||||
{
|
||||
label: 'Djibouti',
|
||||
value: 'DJ',
|
||||
},
|
||||
{
|
||||
label: 'Dominica',
|
||||
value: 'DM',
|
||||
},
|
||||
{
|
||||
label: 'Dominican Republic',
|
||||
value: 'DO',
|
||||
},
|
||||
{
|
||||
label: 'Ecuador',
|
||||
value: 'EC',
|
||||
},
|
||||
{
|
||||
label: 'Egypt',
|
||||
value: 'EG',
|
||||
},
|
||||
{
|
||||
label: 'El Salvador',
|
||||
value: 'SV',
|
||||
},
|
||||
{
|
||||
label: 'Equatorial Guinea',
|
||||
value: 'GQ',
|
||||
},
|
||||
{
|
||||
label: 'Eritrea',
|
||||
value: 'ER',
|
||||
},
|
||||
{
|
||||
label: 'Estonia',
|
||||
value: 'EE',
|
||||
},
|
||||
{
|
||||
label: 'Ethiopia',
|
||||
value: 'ET',
|
||||
},
|
||||
{
|
||||
label: 'Falkland Islands (Malvinas)',
|
||||
value: 'FK',
|
||||
},
|
||||
{
|
||||
label: 'Faroe Islands',
|
||||
value: 'FO',
|
||||
},
|
||||
{
|
||||
label: 'Fiji',
|
||||
value: 'FJ',
|
||||
},
|
||||
{
|
||||
label: 'Finland',
|
||||
value: 'FI',
|
||||
},
|
||||
{
|
||||
label: 'France',
|
||||
value: 'FR',
|
||||
},
|
||||
{
|
||||
label: 'French Guiana',
|
||||
value: 'GF',
|
||||
},
|
||||
{
|
||||
label: 'French Polynesia',
|
||||
value: 'PF',
|
||||
},
|
||||
{
|
||||
label: 'French Southern Territories',
|
||||
value: 'TF',
|
||||
},
|
||||
{
|
||||
label: 'Gabon',
|
||||
value: 'GA',
|
||||
},
|
||||
{
|
||||
label: 'Gambia',
|
||||
value: 'GM',
|
||||
},
|
||||
{
|
||||
label: 'Georgia',
|
||||
value: 'GE',
|
||||
},
|
||||
{
|
||||
label: 'Germany',
|
||||
value: 'DE',
|
||||
},
|
||||
{
|
||||
label: 'Ghana',
|
||||
value: 'GH',
|
||||
},
|
||||
{
|
||||
label: 'Gibraltar',
|
||||
value: 'GI',
|
||||
},
|
||||
{
|
||||
label: 'Greece',
|
||||
value: 'GR',
|
||||
},
|
||||
{
|
||||
label: 'Greenland',
|
||||
value: 'GL',
|
||||
},
|
||||
{
|
||||
label: 'Grenada',
|
||||
value: 'GD',
|
||||
},
|
||||
{
|
||||
label: 'Guadeloupe',
|
||||
value: 'GP',
|
||||
},
|
||||
{
|
||||
label: 'Guam',
|
||||
value: 'GU',
|
||||
},
|
||||
{
|
||||
label: 'Guatemala',
|
||||
value: 'GT',
|
||||
},
|
||||
{
|
||||
label: 'Guernsey',
|
||||
value: 'GG',
|
||||
},
|
||||
{
|
||||
label: 'Guinea',
|
||||
value: 'GN',
|
||||
},
|
||||
{
|
||||
label: 'Guinea-Bissau',
|
||||
value: 'GW',
|
||||
},
|
||||
{
|
||||
label: 'Guyana',
|
||||
value: 'GY',
|
||||
},
|
||||
{
|
||||
label: 'Haiti',
|
||||
value: 'HT',
|
||||
},
|
||||
{
|
||||
label: 'Heard Island and Mcdonald Islands',
|
||||
value: 'HM',
|
||||
},
|
||||
{
|
||||
label: 'Holy See (Vatican City State)',
|
||||
value: 'VA',
|
||||
},
|
||||
{
|
||||
label: 'Honduras',
|
||||
value: 'HN',
|
||||
},
|
||||
{
|
||||
label: 'Hong Kong',
|
||||
value: 'HK',
|
||||
},
|
||||
{
|
||||
label: 'Hungary',
|
||||
value: 'HU',
|
||||
},
|
||||
{
|
||||
label: 'Iceland',
|
||||
value: 'IS',
|
||||
},
|
||||
{
|
||||
label: 'India',
|
||||
value: 'IN',
|
||||
},
|
||||
{
|
||||
label: 'Indonesia',
|
||||
value: 'ID',
|
||||
},
|
||||
{
|
||||
label: 'Iran, Islamic Republic Of',
|
||||
value: 'IR',
|
||||
},
|
||||
{
|
||||
label: 'Iraq',
|
||||
value: 'IQ',
|
||||
},
|
||||
{
|
||||
label: 'Ireland',
|
||||
value: 'IE',
|
||||
},
|
||||
{
|
||||
label: 'Isle of Man',
|
||||
value: 'IM',
|
||||
},
|
||||
{
|
||||
label: 'Israel',
|
||||
value: 'IL',
|
||||
},
|
||||
{
|
||||
label: 'Italy',
|
||||
value: 'IT',
|
||||
},
|
||||
{
|
||||
label: 'Jamaica',
|
||||
value: 'JM',
|
||||
},
|
||||
{
|
||||
label: 'Japan',
|
||||
value: 'JP',
|
||||
},
|
||||
{
|
||||
label: 'Jersey',
|
||||
value: 'JE',
|
||||
},
|
||||
{
|
||||
label: 'Jordan',
|
||||
value: 'JO',
|
||||
},
|
||||
{
|
||||
label: 'Kazakhstan',
|
||||
value: 'KZ',
|
||||
},
|
||||
{
|
||||
label: 'Kenya',
|
||||
value: 'KE',
|
||||
},
|
||||
{
|
||||
label: 'Kiribati',
|
||||
value: 'KI',
|
||||
},
|
||||
{
|
||||
label: "Democratic People's Republic of Korea",
|
||||
value: 'KP',
|
||||
},
|
||||
{
|
||||
label: 'Korea, Republic of',
|
||||
value: 'KR',
|
||||
},
|
||||
{
|
||||
label: 'Kosovo',
|
||||
value: 'XK',
|
||||
},
|
||||
{
|
||||
label: 'Kuwait',
|
||||
value: 'KW',
|
||||
},
|
||||
{
|
||||
label: 'Kyrgyzstan',
|
||||
value: 'KG',
|
||||
},
|
||||
{
|
||||
label: "Lao People's Democratic Republic",
|
||||
value: 'LA',
|
||||
},
|
||||
{
|
||||
label: 'Latvia',
|
||||
value: 'LV',
|
||||
},
|
||||
{
|
||||
label: 'Lebanon',
|
||||
value: 'LB',
|
||||
},
|
||||
{
|
||||
label: 'Lesotho',
|
||||
value: 'LS',
|
||||
},
|
||||
{
|
||||
label: 'Liberia',
|
||||
value: 'LR',
|
||||
},
|
||||
{
|
||||
label: 'Libyan Arab Jamahiriya',
|
||||
value: 'LY',
|
||||
},
|
||||
{
|
||||
label: 'Liechtenstein',
|
||||
value: 'LI',
|
||||
},
|
||||
{
|
||||
label: 'Lithuania',
|
||||
value: 'LT',
|
||||
},
|
||||
{
|
||||
label: 'Luxembourg',
|
||||
value: 'LU',
|
||||
},
|
||||
{
|
||||
label: 'Macao',
|
||||
value: 'MO',
|
||||
},
|
||||
{
|
||||
label: 'Macedonia, The Former Yugoslav Republic of',
|
||||
value: 'MK',
|
||||
},
|
||||
{
|
||||
label: 'Madagascar',
|
||||
value: 'MG',
|
||||
},
|
||||
{
|
||||
label: 'Malawi',
|
||||
value: 'MW',
|
||||
},
|
||||
{
|
||||
label: 'Malaysia',
|
||||
value: 'MY',
|
||||
},
|
||||
{
|
||||
label: 'Maldives',
|
||||
value: 'MV',
|
||||
},
|
||||
{
|
||||
label: 'Mali',
|
||||
value: 'ML',
|
||||
},
|
||||
{
|
||||
label: 'Malta',
|
||||
value: 'MT',
|
||||
},
|
||||
{
|
||||
label: 'Marshall Islands',
|
||||
value: 'MH',
|
||||
},
|
||||
{
|
||||
label: 'Martinique',
|
||||
value: 'MQ',
|
||||
},
|
||||
{
|
||||
label: 'Mauritania',
|
||||
value: 'MR',
|
||||
},
|
||||
{
|
||||
label: 'Mauritius',
|
||||
value: 'MU',
|
||||
},
|
||||
{
|
||||
label: 'Mayotte',
|
||||
value: 'YT',
|
||||
},
|
||||
{
|
||||
label: 'Mexico',
|
||||
value: 'MX',
|
||||
},
|
||||
{
|
||||
label: 'Micronesia, Federated States of',
|
||||
value: 'FM',
|
||||
},
|
||||
{
|
||||
label: 'Moldova, Republic of',
|
||||
value: 'MD',
|
||||
},
|
||||
{
|
||||
label: 'Monaco',
|
||||
value: 'MC',
|
||||
},
|
||||
{
|
||||
label: 'Mongolia',
|
||||
value: 'MN',
|
||||
},
|
||||
{
|
||||
label: 'Montenegro',
|
||||
value: 'ME',
|
||||
},
|
||||
{
|
||||
label: 'Montserrat',
|
||||
value: 'MS',
|
||||
},
|
||||
{
|
||||
label: 'Morocco',
|
||||
value: 'MA',
|
||||
},
|
||||
{
|
||||
label: 'Mozambique',
|
||||
value: 'MZ',
|
||||
},
|
||||
{
|
||||
label: 'Myanmar',
|
||||
value: 'MM',
|
||||
},
|
||||
{
|
||||
label: 'Namibia',
|
||||
value: 'NA',
|
||||
},
|
||||
{
|
||||
label: 'Nauru',
|
||||
value: 'NR',
|
||||
},
|
||||
{
|
||||
label: 'Nepal',
|
||||
value: 'NP',
|
||||
},
|
||||
{
|
||||
label: 'Netherlands',
|
||||
value: 'NL',
|
||||
},
|
||||
{
|
||||
label: 'Netherlands Antilles',
|
||||
value: 'AN',
|
||||
},
|
||||
{
|
||||
label: 'New Caledonia',
|
||||
value: 'NC',
|
||||
},
|
||||
{
|
||||
label: 'New Zealand',
|
||||
value: 'NZ',
|
||||
},
|
||||
{
|
||||
label: 'Nicaragua',
|
||||
value: 'NI',
|
||||
},
|
||||
{
|
||||
label: 'Niger',
|
||||
value: 'NE',
|
||||
},
|
||||
{
|
||||
label: 'Nigeria',
|
||||
value: 'NG',
|
||||
},
|
||||
{
|
||||
label: 'Niue',
|
||||
value: 'NU',
|
||||
},
|
||||
{
|
||||
label: 'Norfolk Island',
|
||||
value: 'NF',
|
||||
},
|
||||
{
|
||||
label: 'Northern Mariana Islands',
|
||||
value: 'MP',
|
||||
},
|
||||
{
|
||||
label: 'Norway',
|
||||
value: 'NO',
|
||||
},
|
||||
{
|
||||
label: 'Oman',
|
||||
value: 'OM',
|
||||
},
|
||||
{
|
||||
label: 'Pakistan',
|
||||
value: 'PK',
|
||||
},
|
||||
{
|
||||
label: 'Palau',
|
||||
value: 'PW',
|
||||
},
|
||||
{
|
||||
label: 'Palestinian Territory, Occupied',
|
||||
value: 'PS',
|
||||
},
|
||||
{
|
||||
label: 'Panama',
|
||||
value: 'PA',
|
||||
},
|
||||
{
|
||||
label: 'Papua New Guinea',
|
||||
value: 'PG',
|
||||
},
|
||||
{
|
||||
label: 'Paraguay',
|
||||
value: 'PY',
|
||||
},
|
||||
{
|
||||
label: 'Peru',
|
||||
value: 'PE',
|
||||
},
|
||||
{
|
||||
label: 'Philippines',
|
||||
value: 'PH',
|
||||
},
|
||||
{
|
||||
label: 'Pitcairn',
|
||||
value: 'PN',
|
||||
},
|
||||
{
|
||||
label: 'Poland',
|
||||
value: 'PL',
|
||||
},
|
||||
{
|
||||
label: 'Portugal',
|
||||
value: 'PT',
|
||||
},
|
||||
{
|
||||
label: 'Puerto Rico',
|
||||
value: 'PR',
|
||||
},
|
||||
{
|
||||
label: 'Qatar',
|
||||
value: 'QA',
|
||||
},
|
||||
{
|
||||
label: 'Reunion',
|
||||
value: 'RE',
|
||||
},
|
||||
{
|
||||
label: 'Romania',
|
||||
value: 'RO',
|
||||
},
|
||||
{
|
||||
label: 'Russian Federation',
|
||||
value: 'RU',
|
||||
},
|
||||
{
|
||||
label: 'Rwanda',
|
||||
value: 'RW',
|
||||
},
|
||||
{
|
||||
label: 'Saint Helena',
|
||||
value: 'SH',
|
||||
},
|
||||
{
|
||||
label: 'Saint Kitts and Nevis',
|
||||
value: 'KN',
|
||||
},
|
||||
{
|
||||
label: 'Saint Lucia',
|
||||
value: 'LC',
|
||||
},
|
||||
{
|
||||
label: 'Saint Pierre and Miquelon',
|
||||
value: 'PM',
|
||||
},
|
||||
{
|
||||
label: 'Saint Vincent and the Grenadines',
|
||||
value: 'VC',
|
||||
},
|
||||
{
|
||||
label: 'Samoa',
|
||||
value: 'WS',
|
||||
},
|
||||
{
|
||||
label: 'San Marino',
|
||||
value: 'SM',
|
||||
},
|
||||
{
|
||||
label: 'Sao Tome and Principe',
|
||||
value: 'ST',
|
||||
},
|
||||
{
|
||||
label: 'Saudi Arabia',
|
||||
value: 'SA',
|
||||
},
|
||||
{
|
||||
label: 'Senegal',
|
||||
value: 'SN',
|
||||
},
|
||||
{
|
||||
label: 'Serbia',
|
||||
value: 'RS',
|
||||
},
|
||||
{
|
||||
label: 'Seychelles',
|
||||
value: 'SC',
|
||||
},
|
||||
{
|
||||
label: 'Sierra Leone',
|
||||
value: 'SL',
|
||||
},
|
||||
{
|
||||
label: 'Singapore',
|
||||
value: 'SG',
|
||||
},
|
||||
{
|
||||
label: 'Slovakia',
|
||||
value: 'SK',
|
||||
},
|
||||
{
|
||||
label: 'Slovenia',
|
||||
value: 'SI',
|
||||
},
|
||||
{
|
||||
label: 'Solomon Islands',
|
||||
value: 'SB',
|
||||
},
|
||||
{
|
||||
label: 'Somalia',
|
||||
value: 'SO',
|
||||
},
|
||||
{
|
||||
label: 'South Africa',
|
||||
value: 'ZA',
|
||||
},
|
||||
{
|
||||
label: 'South Georgia and the South Sandwich Islands',
|
||||
value: 'GS',
|
||||
},
|
||||
{
|
||||
label: 'Spain',
|
||||
value: 'ES',
|
||||
},
|
||||
{
|
||||
label: 'Sri Lanka',
|
||||
value: 'LK',
|
||||
},
|
||||
{
|
||||
label: 'Sudan',
|
||||
value: 'SD',
|
||||
},
|
||||
{
|
||||
label: 'Suriname',
|
||||
value: 'SR',
|
||||
},
|
||||
{
|
||||
label: 'Svalbard and Jan Mayen',
|
||||
value: 'SJ',
|
||||
},
|
||||
{
|
||||
label: 'Swaziland',
|
||||
value: 'SZ',
|
||||
},
|
||||
{
|
||||
label: 'Sweden',
|
||||
value: 'SE',
|
||||
},
|
||||
{
|
||||
label: 'Switzerland',
|
||||
value: 'CH',
|
||||
},
|
||||
{
|
||||
label: 'Syrian Arab Republic',
|
||||
value: 'SY',
|
||||
},
|
||||
{
|
||||
label: 'Taiwan',
|
||||
value: 'TW',
|
||||
},
|
||||
{
|
||||
label: 'Tajikistan',
|
||||
value: 'TJ',
|
||||
},
|
||||
{
|
||||
label: 'Tanzania, United Republic of',
|
||||
value: 'TZ',
|
||||
},
|
||||
{
|
||||
label: 'Thailand',
|
||||
value: 'TH',
|
||||
},
|
||||
{
|
||||
label: 'Timor-Leste',
|
||||
value: 'TL',
|
||||
},
|
||||
{
|
||||
label: 'Togo',
|
||||
value: 'TG',
|
||||
},
|
||||
{
|
||||
label: 'Tokelau',
|
||||
value: 'TK',
|
||||
},
|
||||
{
|
||||
label: 'Tonga',
|
||||
value: 'TO',
|
||||
},
|
||||
{
|
||||
label: 'Trinidad and Tobago',
|
||||
value: 'TT',
|
||||
},
|
||||
{
|
||||
label: 'Tunisia',
|
||||
value: 'TN',
|
||||
},
|
||||
{
|
||||
label: 'Turkey',
|
||||
value: 'TR',
|
||||
},
|
||||
{
|
||||
label: 'Turkmenistan',
|
||||
value: 'TM',
|
||||
},
|
||||
{
|
||||
label: 'Turks and Caicos Islands',
|
||||
value: 'TC',
|
||||
},
|
||||
{
|
||||
label: 'Tuvalu',
|
||||
value: 'TV',
|
||||
},
|
||||
{
|
||||
label: 'Uganda',
|
||||
value: 'UG',
|
||||
},
|
||||
{
|
||||
label: 'Ukraine',
|
||||
value: 'UA',
|
||||
},
|
||||
{
|
||||
label: 'United Arab Emirates',
|
||||
value: 'AE',
|
||||
},
|
||||
{
|
||||
label: 'United Kingdom',
|
||||
value: 'GB',
|
||||
},
|
||||
{
|
||||
label: 'United States',
|
||||
value: 'US',
|
||||
},
|
||||
{
|
||||
label: 'United States Minor Outlying Islands',
|
||||
value: 'UM',
|
||||
},
|
||||
{
|
||||
label: 'Uruguay',
|
||||
value: 'UY',
|
||||
},
|
||||
{
|
||||
label: 'Uzbekistan',
|
||||
value: 'UZ',
|
||||
},
|
||||
{
|
||||
label: 'Vanuatu',
|
||||
value: 'VU',
|
||||
},
|
||||
{
|
||||
label: 'Venezuela',
|
||||
value: 'VE',
|
||||
},
|
||||
{
|
||||
label: 'Viet Nam',
|
||||
value: 'VN',
|
||||
},
|
||||
{
|
||||
label: 'Virgin Islands, British',
|
||||
value: 'VG',
|
||||
},
|
||||
{
|
||||
label: 'Virgin Islands, U.S.',
|
||||
value: 'VI',
|
||||
},
|
||||
{
|
||||
label: 'Wallis and Futuna',
|
||||
value: 'WF',
|
||||
},
|
||||
{
|
||||
label: 'Western Sahara',
|
||||
value: 'EH',
|
||||
},
|
||||
{
|
||||
label: 'Yemen',
|
||||
value: 'YE',
|
||||
},
|
||||
{
|
||||
label: 'Zambia',
|
||||
value: 'ZM',
|
||||
},
|
||||
{
|
||||
label: 'Zimbabwe',
|
||||
value: 'ZW',
|
||||
},
|
||||
]
|
||||
@@ -1,38 +0,0 @@
|
||||
import type { EmailField } from '@payloadcms/plugin-form-builder/types'
|
||||
import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form'
|
||||
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import React from 'react'
|
||||
|
||||
import { Error } from '../Error'
|
||||
import { Width } from '../Width'
|
||||
|
||||
export const Email: React.FC<
|
||||
EmailField & {
|
||||
errors: Partial<FieldErrorsImpl>
|
||||
register: UseFormRegister<FieldValues>
|
||||
}
|
||||
> = ({ name, defaultValue, errors, label, register, required, width }) => {
|
||||
return (
|
||||
<Width width={width}>
|
||||
<Label htmlFor={name}>
|
||||
{label}
|
||||
|
||||
{required && (
|
||||
<span className="required">
|
||||
* <span className="sr-only">(required)</span>
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
<Input
|
||||
defaultValue={defaultValue}
|
||||
id={name}
|
||||
type="text"
|
||||
{...register(name, { pattern: /^\S[^\s@]*@\S+$/, required })}
|
||||
/>
|
||||
|
||||
{errors[name] && <Error name={name} />}
|
||||
</Width>
|
||||
)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { useFormContext } from 'react-hook-form'
|
||||
|
||||
export const Error = ({ name }: { name: string }) => {
|
||||
const {
|
||||
formState: { errors },
|
||||
} = useFormContext()
|
||||
return (
|
||||
<div className="mt-2 text-red-500 text-sm">
|
||||
{(errors[name]?.message as string) || 'This field is required'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import RichText from '@/components/RichText'
|
||||
import React from 'react'
|
||||
|
||||
import { Width } from '../Width'
|
||||
import { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
export const Message: React.FC<{ message: SerializedEditorState }> = ({ message }) => {
|
||||
return (
|
||||
<Width className="my-12" width="100">
|
||||
{message && <RichText data={message} />}
|
||||
</Width>
|
||||
)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import type { TextField } from '@payloadcms/plugin-form-builder/types'
|
||||
import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form'
|
||||
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import React from 'react'
|
||||
|
||||
import { Error } from '../Error'
|
||||
import { Width } from '../Width'
|
||||
export const Number: React.FC<
|
||||
TextField & {
|
||||
errors: Partial<FieldErrorsImpl>
|
||||
register: UseFormRegister<FieldValues>
|
||||
}
|
||||
> = ({ name, defaultValue, errors, label, register, required, width }) => {
|
||||
return (
|
||||
<Width width={width}>
|
||||
<Label htmlFor={name}>
|
||||
{label}
|
||||
|
||||
{required && (
|
||||
<span className="required">
|
||||
* <span className="sr-only">(required)</span>
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
<Input
|
||||
defaultValue={defaultValue}
|
||||
id={name}
|
||||
type="number"
|
||||
{...register(name, { required })}
|
||||
/>
|
||||
{errors[name] && <Error name={name} />}
|
||||
</Width>
|
||||
)
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import type { SelectField } from '@payloadcms/plugin-form-builder/types'
|
||||
import type { Control, FieldErrorsImpl } from 'react-hook-form'
|
||||
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select as SelectComponent,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import React from 'react'
|
||||
import { Controller } from 'react-hook-form'
|
||||
|
||||
import { Error } from '../Error'
|
||||
import { Width } from '../Width'
|
||||
|
||||
export const Select: React.FC<
|
||||
SelectField & {
|
||||
control: Control
|
||||
errors: Partial<FieldErrorsImpl>
|
||||
}
|
||||
> = ({ name, control, errors, label, options, required, width, defaultValue }) => {
|
||||
return (
|
||||
<Width width={width}>
|
||||
<Label htmlFor={name}>
|
||||
{label}
|
||||
{required && (
|
||||
<span className="required">
|
||||
* <span className="sr-only">(required)</span>
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue={defaultValue}
|
||||
name={name}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const controlledValue = options.find((t) => t.value === value)
|
||||
|
||||
return (
|
||||
<SelectComponent onValueChange={(val) => onChange(val)} value={controlledValue?.value}>
|
||||
<SelectTrigger className="w-full" id={name}>
|
||||
<SelectValue placeholder={label} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map(({ label, value }) => {
|
||||
return (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
)
|
||||
})}
|
||||
</SelectContent>
|
||||
</SelectComponent>
|
||||
)
|
||||
}}
|
||||
rules={{ required }}
|
||||
/>
|
||||
{errors[name] && <Error name={name} />}
|
||||
</Width>
|
||||
)
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import type { StateField } from '@payloadcms/plugin-form-builder/types'
|
||||
import type { Control, FieldErrorsImpl } from 'react-hook-form'
|
||||
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import React from 'react'
|
||||
import { Controller } from 'react-hook-form'
|
||||
|
||||
import { Error } from '../Error'
|
||||
import { Width } from '../Width'
|
||||
import { stateOptions } from './options'
|
||||
|
||||
export const State: React.FC<
|
||||
StateField & {
|
||||
control: Control
|
||||
errors: Partial<FieldErrorsImpl>
|
||||
}
|
||||
> = ({ name, control, errors, label, required, width }) => {
|
||||
return (
|
||||
<Width width={width}>
|
||||
<Label htmlFor={name}>
|
||||
{label}
|
||||
{required && (
|
||||
<span className="required">
|
||||
* <span className="sr-only">(required)</span>
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name={name}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const controlledValue = stateOptions.find((t) => t.value === value)
|
||||
|
||||
return (
|
||||
<Select onValueChange={(val) => onChange(val)} value={controlledValue?.value}>
|
||||
<SelectTrigger className="w-full" id={name}>
|
||||
<SelectValue placeholder={label} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{stateOptions.map(({ label, value }) => {
|
||||
return (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
)
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
}}
|
||||
rules={{ required }}
|
||||
/>
|
||||
{errors[name] && <Error name={name} />}
|
||||
</Width>
|
||||
)
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
export const stateOptions = [
|
||||
{ label: 'Alabama', value: 'AL' },
|
||||
{ label: 'Alaska', value: 'AK' },
|
||||
{ label: 'Arizona', value: 'AZ' },
|
||||
{ label: 'Arkansas', value: 'AR' },
|
||||
{ label: 'California', value: 'CA' },
|
||||
{ label: 'Colorado', value: 'CO' },
|
||||
{ label: 'Connecticut', value: 'CT' },
|
||||
{ label: 'Delaware', value: 'DE' },
|
||||
{ label: 'Florida', value: 'FL' },
|
||||
{ label: 'Georgia', value: 'GA' },
|
||||
{ label: 'Hawaii', value: 'HI' },
|
||||
{ label: 'Idaho', value: 'ID' },
|
||||
{ label: 'Illinois', value: 'IL' },
|
||||
{ label: 'Indiana', value: 'IN' },
|
||||
{ label: 'Iowa', value: 'IA' },
|
||||
{ label: 'Kansas', value: 'KS' },
|
||||
{ label: 'Kentucky', value: 'KY' },
|
||||
{ label: 'Louisiana', value: 'LA' },
|
||||
{ label: 'Maine', value: 'ME' },
|
||||
{ label: 'Maryland', value: 'MD' },
|
||||
{ label: 'Massachusetts', value: 'MA' },
|
||||
{ label: 'Michigan', value: 'MI' },
|
||||
{ label: 'Minnesota', value: 'MN' },
|
||||
{ label: 'Mississippi', value: 'MS' },
|
||||
{ label: 'Missouri', value: 'MO' },
|
||||
{ label: 'Montana', value: 'MT' },
|
||||
{ label: 'Nebraska', value: 'NE' },
|
||||
{ label: 'Nevada', value: 'NV' },
|
||||
{ label: 'New Hampshire', value: 'NH' },
|
||||
{ label: 'New Jersey', value: 'NJ' },
|
||||
{ label: 'New Mexico', value: 'NM' },
|
||||
{ label: 'New York', value: 'NY' },
|
||||
{ label: 'North Carolina', value: 'NC' },
|
||||
{ label: 'North Dakota', value: 'ND' },
|
||||
{ label: 'Ohio', value: 'OH' },
|
||||
{ label: 'Oklahoma', value: 'OK' },
|
||||
{ label: 'Oregon', value: 'OR' },
|
||||
{ label: 'Pennsylvania', value: 'PA' },
|
||||
{ label: 'Rhode Island', value: 'RI' },
|
||||
{ label: 'South Carolina', value: 'SC' },
|
||||
{ label: 'South Dakota', value: 'SD' },
|
||||
{ label: 'Tennessee', value: 'TN' },
|
||||
{ label: 'Texas', value: 'TX' },
|
||||
{ label: 'Utah', value: 'UT' },
|
||||
{ label: 'Vermont', value: 'VT' },
|
||||
{ label: 'Virginia', value: 'VA' },
|
||||
{ label: 'Washington', value: 'WA' },
|
||||
{ label: 'West Virginia', value: 'WV' },
|
||||
{ label: 'Wisconsin', value: 'WI' },
|
||||
{ label: 'Wyoming', value: 'WY' },
|
||||
]
|
||||
@@ -1,32 +0,0 @@
|
||||
import type { TextField } from '@payloadcms/plugin-form-builder/types'
|
||||
import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form'
|
||||
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import React from 'react'
|
||||
|
||||
import { Error } from '../Error'
|
||||
import { Width } from '../Width'
|
||||
|
||||
export const Text: React.FC<
|
||||
TextField & {
|
||||
errors: Partial<FieldErrorsImpl>
|
||||
register: UseFormRegister<FieldValues>
|
||||
}
|
||||
> = ({ name, defaultValue, errors, label, register, required, width }) => {
|
||||
return (
|
||||
<Width width={width}>
|
||||
<Label htmlFor={name}>
|
||||
{label}
|
||||
|
||||
{required && (
|
||||
<span className="required">
|
||||
* <span className="sr-only">(required)</span>
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
<Input defaultValue={defaultValue} id={name} type="text" {...register(name, { required })} />
|
||||
{errors[name] && <Error name={name} />}
|
||||
</Width>
|
||||
)
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import type { TextField } from '@payloadcms/plugin-form-builder/types'
|
||||
import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form'
|
||||
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea as TextAreaComponent } from '@/components/ui/textarea'
|
||||
import React from 'react'
|
||||
|
||||
import { Error } from '../Error'
|
||||
import { Width } from '../Width'
|
||||
|
||||
export const Textarea: React.FC<
|
||||
TextField & {
|
||||
errors: Partial<FieldErrorsImpl>
|
||||
register: UseFormRegister<FieldValues>
|
||||
rows?: number
|
||||
}
|
||||
> = ({ name, defaultValue, errors, label, register, required, rows = 3, width }) => {
|
||||
return (
|
||||
<Width width={width}>
|
||||
<Label htmlFor={name}>
|
||||
{label}
|
||||
|
||||
{required && (
|
||||
<span className="required">
|
||||
* <span className="sr-only">(required)</span>
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
|
||||
<TextAreaComponent
|
||||
defaultValue={defaultValue}
|
||||
id={name}
|
||||
rows={rows}
|
||||
{...register(name, { required: required })}
|
||||
/>
|
||||
|
||||
{errors[name] && <Error name={name} />}
|
||||
</Width>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import * as React from 'react'
|
||||
|
||||
export const Width: React.FC<{
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
width?: number | string
|
||||
}> = ({ children, className, width }) => {
|
||||
return (
|
||||
<div className={className} style={{ maxWidth: width ? `${width}%` : undefined }}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import type { Block } from 'payload'
|
||||
|
||||
import {
|
||||
FixedToolbarFeature,
|
||||
HeadingFeature,
|
||||
InlineToolbarFeature,
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
export const FormBlock: Block = {
|
||||
slug: 'formBlock',
|
||||
interfaceName: 'FormBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'form',
|
||||
type: 'relationship',
|
||||
relationTo: 'forms',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'enableIntro',
|
||||
type: 'checkbox',
|
||||
label: 'Enable Intro Content',
|
||||
},
|
||||
{
|
||||
name: 'introContent',
|
||||
type: 'richText',
|
||||
admin: {
|
||||
condition: (_, { enableIntro }) => Boolean(enableIntro),
|
||||
},
|
||||
editor: lexicalEditor({
|
||||
features: ({ rootFeatures }) => {
|
||||
return [
|
||||
...rootFeatures,
|
||||
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
|
||||
FixedToolbarFeature(),
|
||||
InlineToolbarFeature(),
|
||||
]
|
||||
},
|
||||
}),
|
||||
label: 'Intro Content',
|
||||
},
|
||||
],
|
||||
graphQL: {
|
||||
singularName: 'FormBlock',
|
||||
},
|
||||
labels: {
|
||||
plural: 'Form Blocks',
|
||||
singular: 'Form Block',
|
||||
},
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Checkbox } from './Checkbox'
|
||||
import { Country } from './Country'
|
||||
import { Email } from './Email'
|
||||
import { Message } from './Message'
|
||||
import { Number } from './Number'
|
||||
import { Select } from './Select'
|
||||
import { State } from './State'
|
||||
import { Text } from './Text'
|
||||
import { Textarea } from './Textarea'
|
||||
|
||||
export const fields = {
|
||||
checkbox: Checkbox,
|
||||
country: Country,
|
||||
email: Email,
|
||||
message: Message,
|
||||
number: Number,
|
||||
select: Select,
|
||||
state: State,
|
||||
text: Text,
|
||||
textarea: Textarea,
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import clsx from 'clsx'
|
||||
import React from 'react'
|
||||
import RichText from '@/components/RichText'
|
||||
|
||||
import type { Post } from '@/payload-types'
|
||||
|
||||
import { Card } from '../../components/Card'
|
||||
import { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
export type RelatedPostsProps = {
|
||||
className?: string
|
||||
docs?: Post[]
|
||||
introContent?: SerializedEditorState
|
||||
}
|
||||
|
||||
export const RelatedPosts: React.FC<RelatedPostsProps> = (props) => {
|
||||
const { className, docs, introContent } = props
|
||||
|
||||
return (
|
||||
<div className={clsx('lg:container', className)}>
|
||||
{introContent && <RichText data={introContent} enableGutter={false} />}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-8 items-stretch">
|
||||
{docs?.map((doc, index) => {
|
||||
if (typeof doc === 'string') return null
|
||||
|
||||
return <Card key={index} doc={doc} relationTo="posts" showCategories />
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import type { Page } from '@/payload-types'
|
||||
|
||||
import { ArchiveBlock } from '@/blocks/ArchiveBlock/Component'
|
||||
import { CallToActionBlock } from '@/blocks/CallToAction/Component'
|
||||
import { ContentBlock } from '@/blocks/Content/Component'
|
||||
import { FormBlock } from '@/blocks/Form/Component'
|
||||
import { MediaBlock } from '@/blocks/MediaBlock/Component'
|
||||
|
||||
const blockComponents = {
|
||||
archive: ArchiveBlock,
|
||||
content: ContentBlock,
|
||||
cta: CallToActionBlock,
|
||||
formBlock: FormBlock,
|
||||
mediaBlock: MediaBlock,
|
||||
}
|
||||
|
||||
export const RenderBlocks: React.FC<{
|
||||
blocks: Page['layout'][0][]
|
||||
}> = (props) => {
|
||||
const { blocks } = props
|
||||
|
||||
const hasBlocks = blocks && Array.isArray(blocks) && blocks.length > 0
|
||||
|
||||
if (hasBlocks) {
|
||||
return (
|
||||
<Fragment>
|
||||
{blocks.map((block, index) => {
|
||||
const { blockType } = block
|
||||
|
||||
if (blockType && blockType in blockComponents) {
|
||||
const Block = blockComponents[blockType]
|
||||
|
||||
if (Block) {
|
||||
return (
|
||||
<div className="my-16" key={index}>
|
||||
{/* @ts-expect-error there may be some mismatch between the expected types here */}
|
||||
<Block {...block} disableInnerContainer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
return null
|
||||
})}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CollectionConfig } from 'payload'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Authors: CollectionConfig = {
|
||||
slug: 'authors',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CollectionConfig } from 'payload'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Categories: CollectionConfig = {
|
||||
slug: 'categories',
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { CollectionAfterChangeHook, CollectionAfterDeleteHook } from 'payload'
|
||||
|
||||
import { revalidatePath, revalidateTag } from 'next/cache'
|
||||
// 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> = ({
|
||||
export const revalidateCustomer: CollectionAfterChangeHook<Customer> = async ({
|
||||
doc,
|
||||
previousDoc,
|
||||
req: { payload, context },
|
||||
@@ -14,9 +14,13 @@ export const revalidateCustomer: CollectionAfterChangeHook<Customer> = ({
|
||||
const path = `/customers/${doc.slug}`
|
||||
|
||||
payload.logger.info(`Revalidating event at path: ${path}`)
|
||||
|
||||
revalidatePath(path)
|
||||
revalidateTag('customers-sitemap')
|
||||
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
|
||||
@@ -25,22 +29,27 @@ export const revalidateCustomer: CollectionAfterChangeHook<Customer> = ({
|
||||
|
||||
payload.logger.info(`Revalidating old event at path: ${oldPath}`)
|
||||
|
||||
revalidatePath(oldPath)
|
||||
revalidateTag('customers-sitemap')
|
||||
try {
|
||||
const { revalidatePath, revalidateTag } = await import('next/cache')
|
||||
revalidatePath(oldPath)
|
||||
revalidateTag('customers-sitemap')
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
export const revalidateDelete: CollectionAfterDeleteHook<Customer> = ({
|
||||
export const revalidateDelete: CollectionAfterDeleteHook<Customer> = async ({
|
||||
doc,
|
||||
req: { context },
|
||||
}) => {
|
||||
if (!context.disableRevalidate) {
|
||||
const path = `/customers/${doc?.slug}`
|
||||
|
||||
revalidatePath(path)
|
||||
revalidateTag('customers-sitemap')
|
||||
try {
|
||||
const { revalidatePath, revalidateTag } = await import('next/cache')
|
||||
revalidatePath(path)
|
||||
revalidateTag('customers-sitemap')
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return doc
|
||||
|
||||
@@ -9,15 +9,15 @@ import {
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { isAnyone } from '@/access/isAnyone'
|
||||
import { isAuthenticated } from '@/access/isAuthenticated'
|
||||
import { isAnyone } from '../../access/isAnyone.ts'
|
||||
import { isAuthenticated } from '../../access/isAuthenticated.ts'
|
||||
|
||||
import { Banner } from '../../blocks/Banner/config'
|
||||
import { Code } from '../../blocks/Code/config'
|
||||
import { MediaBlock } from '../../blocks/MediaBlock/config'
|
||||
import { Quote } from '../../blocks/Quote/config'
|
||||
import { YouTube } from '../../blocks/YouTube/config'
|
||||
import { revalidateDelete, revalidateCustomer } from './hooks/revalidateCustomer'
|
||||
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 { revalidateDelete, revalidateCustomer } from './hooks/revalidateCustomer.ts'
|
||||
|
||||
import {
|
||||
MetaDescriptionField,
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
OverviewField,
|
||||
PreviewField,
|
||||
} from '@payloadcms/plugin-seo/fields'
|
||||
import { slugField } from '@/fields/slug'
|
||||
import { slugField } from '../../fields/slug/index.ts'
|
||||
|
||||
const industryOptions = [
|
||||
{ label: 'Healthcare', value: 'healthcare' },
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { CollectionAfterChangeHook, CollectionAfterDeleteHook } from 'payload'
|
||||
|
||||
import { revalidatePath, revalidateTag } from 'next/cache'
|
||||
// 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> = ({
|
||||
export const revalidateEvent: CollectionAfterChangeHook<Event> = async ({
|
||||
doc,
|
||||
previousDoc,
|
||||
req: { payload, context },
|
||||
@@ -14,9 +14,11 @@ export const revalidateEvent: CollectionAfterChangeHook<Event> = ({
|
||||
const path = `/events/${doc.slug}`
|
||||
|
||||
payload.logger.info(`Revalidating event at path: ${path}`)
|
||||
|
||||
revalidatePath(path)
|
||||
revalidateTag('events-sitemap')
|
||||
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
|
||||
@@ -25,19 +27,27 @@ export const revalidateEvent: CollectionAfterChangeHook<Event> = ({
|
||||
|
||||
payload.logger.info(`Revalidating old event at path: ${oldPath}`)
|
||||
|
||||
revalidatePath(oldPath)
|
||||
revalidateTag('events-sitemap')
|
||||
try {
|
||||
const { revalidatePath, revalidateTag } = await import('next/cache')
|
||||
revalidatePath(oldPath)
|
||||
revalidateTag('events-sitemap')
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
export const revalidateDelete: CollectionAfterDeleteHook<Event> = ({ doc, req: { context } }) => {
|
||||
export const revalidateDelete: CollectionAfterDeleteHook<Event> = async ({
|
||||
doc,
|
||||
req: { context },
|
||||
}) => {
|
||||
if (!context.disableRevalidate) {
|
||||
const path = `/events/${doc?.slug}`
|
||||
|
||||
revalidatePath(path)
|
||||
revalidateTag('events-sitemap')
|
||||
try {
|
||||
const { revalidatePath, revalidateTag } = await import('next/cache')
|
||||
revalidatePath(path)
|
||||
revalidateTag('events-sitemap')
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return doc
|
||||
|
||||
@@ -9,15 +9,15 @@ import {
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { isAnyone } from '@/access/isAnyone'
|
||||
import { isAuthenticated } from '@/access/isAuthenticated'
|
||||
import { isAnyone } from '../../access/isAnyone.ts'
|
||||
import { isAuthenticated } from '../../access/isAuthenticated.ts'
|
||||
|
||||
import { Banner } from '@/blocks/Banner/config'
|
||||
import { Code } from '@/blocks/Code/config'
|
||||
import { MediaBlock } from '@/blocks/MediaBlock/config'
|
||||
import { Quote } from '@/blocks/Quote/config'
|
||||
import { YouTube } from '@/blocks/YouTube/config'
|
||||
import { revalidateDelete, revalidateEvent } from './hooks/revalidateEvent'
|
||||
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 { revalidateDelete, revalidateEvent } from './hooks/revalidateEvent.ts'
|
||||
|
||||
import {
|
||||
MetaDescriptionField,
|
||||
@@ -26,8 +26,8 @@ import {
|
||||
OverviewField,
|
||||
PreviewField,
|
||||
} from '@payloadcms/plugin-seo/fields'
|
||||
import { slugField } from '@/fields/slug'
|
||||
import { timezoneOptions } from '../../utilities/timezones'
|
||||
import { slugField } from '../../fields/slug/index.ts'
|
||||
import { timezoneOptions } from '../../utilities/timezones.ts'
|
||||
|
||||
const eventTypeOptions = [
|
||||
{ label: 'Conference', value: 'conference' },
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { isAnyone } from '@/access/isAnyone'
|
||||
import { isAuthenticated } from '@/access/isAuthenticated'
|
||||
import { isAnyone } from '../access/isAnyone.ts'
|
||||
import { isAuthenticated } from '../access/isAuthenticated.ts'
|
||||
|
||||
export const Media: CollectionConfig = {
|
||||
slug: 'media',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CollectionAfterReadHook } from 'payload'
|
||||
import { Author } from 'src/payload-types'
|
||||
import type { Author } from '../../../payload-types'
|
||||
|
||||
export const populateAuthors: CollectionAfterReadHook = async ({ doc, req: { payload } }) => {
|
||||
if (doc?.authors && doc?.authors?.length > 0) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { CollectionAfterChangeHook, CollectionAfterDeleteHook } from 'payload'
|
||||
|
||||
import { revalidatePath, revalidateTag } from 'next/cache'
|
||||
// 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> = ({
|
||||
export const revalidatePost: CollectionAfterChangeHook<Post> = async ({
|
||||
doc,
|
||||
previousDoc,
|
||||
req: { payload, context },
|
||||
@@ -14,9 +14,11 @@ export const revalidatePost: CollectionAfterChangeHook<Post> = ({
|
||||
const path = `/posts/${doc.slug}`
|
||||
|
||||
payload.logger.info(`Revalidating post at path: ${path}`)
|
||||
|
||||
revalidatePath(path)
|
||||
revalidateTag('posts-sitemap')
|
||||
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
|
||||
@@ -25,19 +27,27 @@ export const revalidatePost: CollectionAfterChangeHook<Post> = ({
|
||||
|
||||
payload.logger.info(`Revalidating old post at path: ${oldPath}`)
|
||||
|
||||
revalidatePath(oldPath)
|
||||
revalidateTag('posts-sitemap')
|
||||
try {
|
||||
const { revalidatePath, revalidateTag } = await import('next/cache')
|
||||
revalidatePath(oldPath)
|
||||
revalidateTag('posts-sitemap')
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
export const revalidateDelete: CollectionAfterDeleteHook<Post> = ({ doc, req: { context } }) => {
|
||||
export const revalidateDelete: CollectionAfterDeleteHook<Post> = async ({
|
||||
doc,
|
||||
req: { context },
|
||||
}) => {
|
||||
if (!context.disableRevalidate) {
|
||||
const path = `/posts/${doc?.slug}`
|
||||
|
||||
revalidatePath(path)
|
||||
revalidateTag('posts-sitemap')
|
||||
try {
|
||||
const { revalidatePath, revalidateTag } = await import('next/cache')
|
||||
revalidatePath(path)
|
||||
revalidateTag('posts-sitemap')
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return doc
|
||||
|
||||
@@ -9,16 +9,16 @@ import {
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { isAnyone } from '@/access/isAnyone'
|
||||
import { isAuthenticated } from '@/access/isAuthenticated'
|
||||
import { isAnyone } from '../../access/isAnyone.ts'
|
||||
import { isAuthenticated } from '../../access/isAuthenticated.ts'
|
||||
|
||||
import { Banner } from '@/blocks/Banner/config'
|
||||
import { Code } from '@/blocks/Code/config'
|
||||
import { MediaBlock } from '@/blocks/MediaBlock/config'
|
||||
import { Quote } from '@/blocks/Quote/config'
|
||||
import { YouTube } from '@/blocks/YouTube/config'
|
||||
import { populateAuthors } from './hooks/populateAuthors'
|
||||
import { revalidateDelete, revalidatePost } from './hooks/revalidatePost'
|
||||
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 { populateAuthors } from './hooks/populateAuthors.ts'
|
||||
import { revalidateDelete, revalidatePost } from './hooks/revalidatePost.ts'
|
||||
|
||||
import {
|
||||
MetaDescriptionField,
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
OverviewField,
|
||||
PreviewField,
|
||||
} from '@payloadcms/plugin-seo/fields'
|
||||
import { slugField } from '@/fields/slug'
|
||||
import { slugField } from '../../fields/slug/index.ts'
|
||||
|
||||
const launchweekOptions = [
|
||||
{ label: '6', value: '6' },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CollectionConfig } from 'payload'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Tags: CollectionConfig = {
|
||||
slug: 'tags',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import { isAdmin, isAdminFieldLevel } from '@/access/isAdmin'
|
||||
import { isAdminOrSelf } from '@/access/isAdminOrSelf'
|
||||
import { isAdmin, isAdminFieldLevel } from '../access/isAdmin.ts'
|
||||
import { isAdminOrSelf } from '../access/isAdminOrSelf.ts'
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
|
||||
@@ -35,7 +35,7 @@ export const Card: React.FC<{
|
||||
'border border-border rounded-lg overflow-hidden bg-card hover:cursor-pointer',
|
||||
className
|
||||
)}
|
||||
ref={card.ref}
|
||||
ref={card.ref as React.Ref<HTMLDivElement>}
|
||||
>
|
||||
<div className="relative w-full ">
|
||||
{!metaImage && <div className="">No image</div>}
|
||||
@@ -48,15 +48,15 @@ export const Card: React.FC<{
|
||||
<div>
|
||||
{categories?.map((category, index) => {
|
||||
if (typeof category === 'object') {
|
||||
const { title: titleFromCategory } = category
|
||||
const { name: nameFromCategory } = category
|
||||
|
||||
const categoryTitle = titleFromCategory || 'Untitled category'
|
||||
const categoryName = nameFromCategory || 'Untitled category'
|
||||
|
||||
const isLast = index === categories.length - 1
|
||||
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
{categoryTitle}
|
||||
{categoryName}
|
||||
{!isLast && <Fragment>, </Fragment>}
|
||||
</Fragment>
|
||||
)
|
||||
@@ -71,7 +71,11 @@ export const Card: React.FC<{
|
||||
{titleToUse && (
|
||||
<div className="prose">
|
||||
<h3>
|
||||
<Link className="not-prose" href={href} ref={link.ref}>
|
||||
<Link
|
||||
className="not-prose"
|
||||
href={href}
|
||||
ref={link.ref as React.Ref<HTMLAnchorElement>}
|
||||
>
|
||||
{titleToUse}
|
||||
</Link>
|
||||
</h3>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { cn } from '@/utilities/ui'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
import type { Page, Post } from '@/payload-types'
|
||||
import type { Post } from '@/payload-types'
|
||||
|
||||
type CMSLinkType = {
|
||||
appearance?: 'inline' | ButtonProps['variant']
|
||||
@@ -12,8 +12,8 @@ type CMSLinkType = {
|
||||
label?: string | null
|
||||
newTab?: boolean | null
|
||||
reference?: {
|
||||
relationTo: 'pages' | 'posts'
|
||||
value: Page | Post | string | number
|
||||
relationTo: 'posts'
|
||||
value: Post | string | number
|
||||
} | null
|
||||
size?: ButtonProps['size'] | null
|
||||
type?: 'custom' | 'reference' | null
|
||||
@@ -35,9 +35,7 @@ export const CMSLink: React.FC<CMSLinkType> = (props) => {
|
||||
|
||||
const href =
|
||||
type === 'reference' && typeof reference?.value === 'object' && reference.value.slug
|
||||
? `${reference?.relationTo !== 'pages' ? `/${reference?.relationTo}` : ''}/${
|
||||
reference.value.slug
|
||||
}`
|
||||
? `/${reference.value.slug}`
|
||||
: url
|
||||
|
||||
if (!href) return null
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import type React from 'react'
|
||||
import type { Post } from '@/payload-types'
|
||||
|
||||
import { getCachedDocument } from '@/utilities/getDocument'
|
||||
import { getCachedRedirects } from '@/utilities/getRedirects'
|
||||
import { notFound, redirect } from 'next/navigation'
|
||||
|
||||
interface Props {
|
||||
disableNotFound?: boolean
|
||||
url: string
|
||||
}
|
||||
|
||||
/* This component helps us with SSR based dynamic redirects */
|
||||
export const PayloadRedirects: React.FC<Props> = async ({ disableNotFound, url }) => {
|
||||
const redirects = await getCachedRedirects()()
|
||||
|
||||
const redirectItem = redirects.find((redirect) => redirect.from === url)
|
||||
|
||||
if (redirectItem) {
|
||||
if (redirectItem.to?.url) {
|
||||
redirect(redirectItem.to.url)
|
||||
}
|
||||
|
||||
let redirectUrl: string
|
||||
|
||||
if (typeof redirectItem.to?.reference?.value === 'string') {
|
||||
const collection = redirectItem.to?.reference?.relationTo
|
||||
const id = redirectItem.to?.reference?.value
|
||||
|
||||
const document = (await getCachedDocument(collection, id)()) as Post
|
||||
redirectUrl = `${redirectItem.to?.reference?.relationTo !== 'pages' ? `/${redirectItem.to?.reference?.relationTo}` : ''}/${
|
||||
document?.slug
|
||||
}`
|
||||
} else {
|
||||
redirectUrl = `${redirectItem.to?.reference?.relationTo !== 'pages' ? `/${redirectItem.to?.reference?.relationTo}` : ''}/${
|
||||
typeof redirectItem.to?.reference?.value === 'object'
|
||||
? redirectItem.to?.reference?.value?.slug
|
||||
: ''
|
||||
}`
|
||||
}
|
||||
|
||||
if (redirectUrl) redirect(redirectUrl)
|
||||
}
|
||||
|
||||
if (disableNotFound) return null
|
||||
|
||||
notFound()
|
||||
}
|
||||
@@ -15,13 +15,11 @@ import { CodeBlock, CodeBlockProps } from '@/blocks/Code/Component'
|
||||
|
||||
import type {
|
||||
BannerBlock as BannerBlockProps,
|
||||
CallToActionBlock as CTABlockProps,
|
||||
MediaBlock as MediaBlockProps,
|
||||
QuoteBlock as QuoteBlockProps,
|
||||
YouTubeBlock as YouTubeBlockProps,
|
||||
} from '@/payload-types'
|
||||
import { BannerBlock } from '@/blocks/Banner/Component'
|
||||
import { CallToActionBlock } from '@/blocks/CallToAction/Component'
|
||||
import { QuoteBlock } from '@/blocks/Quote/Component'
|
||||
import { YouTubeBlock } from '@/blocks/YouTube/Component'
|
||||
import { cn } from '@/utilities/ui'
|
||||
@@ -29,12 +27,7 @@ import { cn } from '@/utilities/ui'
|
||||
type NodeTypes =
|
||||
| DefaultNodeTypes
|
||||
| SerializedBlockNode<
|
||||
| CTABlockProps
|
||||
| MediaBlockProps
|
||||
| BannerBlockProps
|
||||
| CodeBlockProps
|
||||
| QuoteBlockProps
|
||||
| YouTubeBlockProps
|
||||
MediaBlockProps | BannerBlockProps | CodeBlockProps | QuoteBlockProps | YouTubeBlockProps
|
||||
>
|
||||
|
||||
const internalDocToHref = ({ linkNode }: { linkNode: SerializedLinkNode }) => {
|
||||
@@ -62,7 +55,6 @@ const jsxConverters: JSXConvertersFunction<NodeTypes> = ({ defaultConverters })
|
||||
/>
|
||||
),
|
||||
code: ({ node }) => <CodeBlock className="col-start-2" {...node.fields} />,
|
||||
cta: ({ node }) => <CallToActionBlock {...node.fields} />,
|
||||
quote: ({ node }) => <QuoteBlock className="col-start-2" {...node.fields} />,
|
||||
youtube: ({ node }) => <YouTubeBlock className="col-start-2" {...node.fields} />,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Field, GroupField } from 'payload'
|
||||
|
||||
import deepMerge from '@/utilities/deepMerge'
|
||||
import deepMerge from '../utilities/deepMerge.ts'
|
||||
|
||||
export type LinkAppearances = 'default' | 'outline'
|
||||
|
||||
@@ -75,7 +75,7 @@ export const link: LinkType = ({ appearances, disableLabel = false, overrides =
|
||||
condition: (_, siblingData) => siblingData?.type === 'reference',
|
||||
},
|
||||
label: 'Document to link to',
|
||||
relationTo: ['pages', 'posts'],
|
||||
relationTo: ['posts'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { ArrayField, Field } from 'payload'
|
||||
|
||||
import type { LinkAppearances } from './link'
|
||||
import type { LinkAppearances } from './link.ts'
|
||||
|
||||
import deepMerge from '@/utilities/deepMerge'
|
||||
import { link } from './link'
|
||||
import deepMerge from '../utilities/deepMerge.ts'
|
||||
import { link } from './link.ts'
|
||||
|
||||
type LinkGroupType = (options?: {
|
||||
appearances?: LinkAppearances[] | false
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CheckboxField, TextField } from 'payload'
|
||||
|
||||
import { formatSlugHook } from './formatSlug'
|
||||
import { formatSlugHook } from './formatSlug.ts'
|
||||
|
||||
type Overrides = {
|
||||
slugOverrides?: Partial<TextField>
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+154
-221
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
@@ -0,0 +1,15 @@
|
||||
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
@@ -0,0 +1,21 @@
|
||||
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;`)
|
||||
}
|
||||
@@ -1,9 +1,21 @@
|
||||
import * as migration_20250529_103319 from './20250529_103319';
|
||||
import * as migration_20250619_155345 from './20250619_155345.ts';
|
||||
import * as migration_20250619_160549 from './20250619_160549.ts';
|
||||
import * as migration_20250808_145722_update_payload_migrations from './20250808_145722_update_payload_migrations.ts';
|
||||
|
||||
export const migrations = [
|
||||
{
|
||||
up: migration_20250529_103319.up,
|
||||
down: migration_20250529_103319.down,
|
||||
name: '20250529_103319'
|
||||
up: migration_20250619_155345.up,
|
||||
down: migration_20250619_155345.down,
|
||||
name: '20250619_155345',
|
||||
},
|
||||
{
|
||||
up: migration_20250619_160549.up,
|
||||
down: migration_20250619_160549.down,
|
||||
name: '20250619_160549',
|
||||
},
|
||||
{
|
||||
up: migration_20250808_145722_update_payload_migrations.up,
|
||||
down: migration_20250808_145722_update_payload_migrations.down,
|
||||
name: '20250808_145722_update_payload_migrations'
|
||||
},
|
||||
];
|
||||
@@ -987,6 +987,13 @@ export interface User {
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
sessions?:
|
||||
| {
|
||||
id: string;
|
||||
createdAt?: string | null;
|
||||
expiresAt: string;
|
||||
}[]
|
||||
| null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
@@ -1454,6 +1461,13 @@ export interface UsersSelect<T extends boolean = true> {
|
||||
hash?: T;
|
||||
loginAttempts?: T;
|
||||
lockUntil?: T;
|
||||
sessions?:
|
||||
| T
|
||||
| {
|
||||
id?: T;
|
||||
createdAt?: T;
|
||||
expiresAt?: T;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
|
||||
@@ -1,23 +1,78 @@
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
import path from 'path'
|
||||
import { buildConfig } from 'payload'
|
||||
import { fileURLToPath } from 'url'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import sharp from 'sharp'
|
||||
import { plugins } from './plugins'
|
||||
import { defaultLexical } from '@/fields/defaultLexical'
|
||||
import { getServerSideURL } from './utilities/getURL'
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
import { payloadCloudPlugin } from '@payloadcms/payload-cloud'
|
||||
import { nestedDocsPlugin } from '@payloadcms/plugin-nested-docs'
|
||||
import { seoPlugin } from '@payloadcms/plugin-seo'
|
||||
import { s3Storage } from '@payloadcms/storage-s3'
|
||||
import { buildConfig, type Plugin } from 'payload'
|
||||
import { defaultLexical } from './fields/defaultLexical.ts'
|
||||
import { getServerSideURL } from './utilities/getURL.ts'
|
||||
|
||||
import { Authors } from './collections/Authors'
|
||||
import { Categories } from './collections/Categories'
|
||||
import { Customers } from './collections/Customers'
|
||||
import { Events } from './collections/Events'
|
||||
import { Media } from './collections/Media'
|
||||
import { Posts } from './collections/Posts'
|
||||
import { Tags } from './collections/Tags'
|
||||
import { Users } from './collections/Users'
|
||||
import type { GenerateTitle, GenerateURL } from '@payloadcms/plugin-seo/types'
|
||||
import type { Customer, Event, Post } from './payload-types'
|
||||
|
||||
import { Authors } from './collections/Authors.ts'
|
||||
import { Categories } from './collections/Categories.ts'
|
||||
import { Customers } from './collections/Customers/index.ts'
|
||||
import { Events } from './collections/Events/index.ts'
|
||||
import { Media } from './collections/Media.ts'
|
||||
import { Posts } from './collections/Posts/index.ts'
|
||||
import { Tags } from './collections/Tags.ts'
|
||||
import { Users } from './collections/Users.ts'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
const siteName = 'Supabase'
|
||||
|
||||
const generateTitle: GenerateTitle<Post & Customer & Event> = ({ doc, collectionSlug }: any) => {
|
||||
switch (collectionSlug) {
|
||||
case 'customers':
|
||||
return `${doc.name} | ${siteName} Customer Stories`
|
||||
case 'events':
|
||||
return `${doc.title} | ${siteName} Events`
|
||||
case 'posts':
|
||||
return doc.title
|
||||
default:
|
||||
return `${doc.title} | ${siteName}`
|
||||
}
|
||||
}
|
||||
|
||||
const generateURL: GenerateURL<Post> = ({ doc }: any) => {
|
||||
const url = getServerSideURL()
|
||||
|
||||
return doc?.slug ? `${url}/${doc.slug}` : url
|
||||
}
|
||||
|
||||
const plugins: Plugin[] = [
|
||||
nestedDocsPlugin({
|
||||
collections: ['categories'],
|
||||
generateURL: (docs) => docs.reduce((url, doc) => `${url}/${doc.slug}`, ''),
|
||||
}),
|
||||
seoPlugin({
|
||||
generateTitle,
|
||||
generateURL,
|
||||
}),
|
||||
payloadCloudPlugin(),
|
||||
s3Storage({
|
||||
collections: {
|
||||
media: {
|
||||
prefix: 'media',
|
||||
},
|
||||
},
|
||||
bucket: process.env.S3_BUCKET || '',
|
||||
config: {
|
||||
forcePathStyle: true,
|
||||
credentials: {
|
||||
accessKeyId: process.env.S3_ACCESS_KEY_ID || '',
|
||||
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY || '',
|
||||
},
|
||||
region: process.env.S3_REGION,
|
||||
endpoint: process.env.S3_ENDPOINT,
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
||||
export default buildConfig({
|
||||
admin: {
|
||||
@@ -59,7 +114,7 @@ export default buildConfig({
|
||||
pool: {
|
||||
connectionString: process.env.DATABASE_URI || '',
|
||||
},
|
||||
schemaName: 'cms-payload',
|
||||
// schemaName: 'cms-payload',
|
||||
}),
|
||||
sharp,
|
||||
plugins,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
export default buildConfig({
|
||||
secret: process.env.PAYLOAD_SECRET,
|
||||
db: postgresAdapter({
|
||||
pool: {
|
||||
connectionString: process.env.DATABASE_URI || '',
|
||||
},
|
||||
}),
|
||||
})
|
||||
@@ -1,87 +0,0 @@
|
||||
import { payloadCloudPlugin } from '@payloadcms/payload-cloud'
|
||||
// import { formBuilderPlugin } from '@payloadcms/plugin-form-builder'
|
||||
import { nestedDocsPlugin } from '@payloadcms/plugin-nested-docs'
|
||||
import { seoPlugin } from '@payloadcms/plugin-seo'
|
||||
import { s3Storage } from '@payloadcms/storage-s3'
|
||||
import { Plugin } from 'payload'
|
||||
import { GenerateTitle, GenerateURL } from '@payloadcms/plugin-seo/types'
|
||||
// import { FixedToolbarFeature, HeadingFeature, lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { Customer, Event, Post } from '@/payload-types'
|
||||
import { getServerSideURL } from '@/utilities/getURL'
|
||||
|
||||
const siteName = 'Supabase'
|
||||
|
||||
const generateTitle: GenerateTitle<Post & Customer & Event> = ({ doc, collectionSlug }) => {
|
||||
switch (collectionSlug) {
|
||||
case 'customers':
|
||||
return `${doc.name} | ${siteName} Customer Stories`
|
||||
case 'events':
|
||||
return `${doc.title} | ${siteName} Events`
|
||||
case 'posts':
|
||||
return doc.title
|
||||
default:
|
||||
return `${doc.title} | ${siteName}`
|
||||
}
|
||||
}
|
||||
|
||||
const generateURL: GenerateURL<Post> = ({ doc }) => {
|
||||
const url = getServerSideURL()
|
||||
|
||||
return doc?.slug ? `${url}/${doc.slug}` : url
|
||||
}
|
||||
|
||||
export const plugins: Plugin[] = [
|
||||
nestedDocsPlugin({
|
||||
collections: ['categories'],
|
||||
generateURL: (docs) => docs.reduce((url, doc) => `${url}/${doc.slug}`, ''),
|
||||
}),
|
||||
seoPlugin({
|
||||
generateTitle,
|
||||
generateURL,
|
||||
}),
|
||||
// formBuilderPlugin({
|
||||
// fields: {
|
||||
// payment: false,
|
||||
// },
|
||||
// formOverrides: {
|
||||
// fields: ({ defaultFields }) => {
|
||||
// return defaultFields.map((field) => {
|
||||
// if ('name' in field && field.name === 'confirmationMessage') {
|
||||
// return {
|
||||
// ...field,
|
||||
// editor: lexicalEditor({
|
||||
// features: ({ rootFeatures }) => {
|
||||
// return [
|
||||
// ...rootFeatures,
|
||||
// FixedToolbarFeature(),
|
||||
// HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
|
||||
// ]
|
||||
// },
|
||||
// }),
|
||||
// }
|
||||
// }
|
||||
// return field
|
||||
// })
|
||||
// },
|
||||
// },
|
||||
// }),
|
||||
payloadCloudPlugin(),
|
||||
s3Storage({
|
||||
collections: {
|
||||
media: {
|
||||
prefix: 'media',
|
||||
},
|
||||
},
|
||||
bucket: process.env.S3_BUCKET || '',
|
||||
config: {
|
||||
forcePathStyle: true,
|
||||
credentials: {
|
||||
accessKeyId: process.env.S3_ACCESS_KEY_ID || '',
|
||||
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY || '',
|
||||
},
|
||||
region: process.env.S3_REGION,
|
||||
endpoint: process.env.S3_ENDPOINT,
|
||||
},
|
||||
}),
|
||||
]
|
||||
@@ -27,7 +27,11 @@ export const HeaderThemeProvider = ({ children }: { children: React.ReactNode })
|
||||
setThemeState(themeToSet)
|
||||
}, [])
|
||||
|
||||
return <HeaderThemeContext value={{ headerTheme, setHeaderTheme }}>{children}</HeaderThemeContext>
|
||||
return (
|
||||
<HeaderThemeContext.Provider value={{ headerTheme, setHeaderTheme }}>
|
||||
{children}
|
||||
</HeaderThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useHeaderTheme = (): ContextType => use(HeaderThemeContext)
|
||||
@@ -51,7 +51,7 @@ export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
setThemeState(themeToSet)
|
||||
}, [])
|
||||
|
||||
return <ThemeContext value={{ setTheme, theme }}>{children}</ThemeContext>
|
||||
return <ThemeContext.Provider value={{ setTheme, theme }}>{children}</ThemeContext.Provider>
|
||||
}
|
||||
|
||||
export const useTheme = (): ThemeContextType => use(ThemeContext)
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Theme } from './types'
|
||||
import type { Theme } from './types.ts'
|
||||
|
||||
export const themeLocalStorageKey = 'payload-theme'
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
'use client'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useDebounce } from '@/utilities/useDebounce'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
export const Search: React.FC = () => {
|
||||
const [value, setValue] = useState('')
|
||||
const router = useRouter()
|
||||
|
||||
const debouncedValue = useDebounce(value)
|
||||
|
||||
useEffect(() => {
|
||||
router.push(`/search${debouncedValue ? `?q=${debouncedValue}` : ''}`)
|
||||
}, [debouncedValue, router])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
<Label htmlFor="search" className="sr-only">
|
||||
Search
|
||||
</Label>
|
||||
<Input
|
||||
id="search"
|
||||
onChange={(event) => {
|
||||
setValue(event.target.value)
|
||||
}}
|
||||
placeholder="Search"
|
||||
/>
|
||||
<button type="submit" className="sr-only">
|
||||
submit
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { BeforeSync, DocToSync } from '@payloadcms/plugin-search/types'
|
||||
|
||||
export const beforeSyncWithSearch: BeforeSync = async ({ originalDoc, searchDoc }) => {
|
||||
const {
|
||||
doc: { relationTo: collection },
|
||||
} = searchDoc
|
||||
|
||||
const { slug, id, categories, title, meta } = originalDoc
|
||||
|
||||
const modifiedDoc: DocToSync = {
|
||||
...searchDoc,
|
||||
slug,
|
||||
meta: {
|
||||
...meta,
|
||||
title: meta?.title || title,
|
||||
image: meta?.image?.id || meta?.image,
|
||||
description: meta?.description,
|
||||
},
|
||||
categories: [],
|
||||
}
|
||||
|
||||
if (categories && Array.isArray(categories) && categories.length > 0) {
|
||||
// get full categories and keep a flattened copy of their most important properties
|
||||
try {
|
||||
const mappedCategories = categories.map((category) => {
|
||||
const { id, title } = category
|
||||
|
||||
return {
|
||||
relationTo: 'categories',
|
||||
id,
|
||||
title,
|
||||
}
|
||||
})
|
||||
|
||||
modifiedDoc.categories = mappedCategories
|
||||
} catch (_err) {
|
||||
console.error(
|
||||
`Failed. Category not found when syncing collection '${collection}' with id: '${id}' to search.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return modifiedDoc
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Field } from 'payload'
|
||||
|
||||
export const searchFields: Field[] = [
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'text',
|
||||
index: true,
|
||||
admin: {
|
||||
readOnly: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'meta',
|
||||
label: 'Meta',
|
||||
type: 'group',
|
||||
index: true,
|
||||
admin: {
|
||||
readOnly: true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'title',
|
||||
label: 'Title',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
},
|
||||
{
|
||||
name: 'image',
|
||||
label: 'Image',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Categories',
|
||||
name: 'categories',
|
||||
type: 'array',
|
||||
admin: {
|
||||
readOnly: true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'relationTo',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Post } from '@/payload-types'
|
||||
|
||||
/**
|
||||
* Formats an array of populatedAuthors from Posts into a prettified string.
|
||||
* @param authors - The populatedAuthors array from a Post.
|
||||
* @returns A prettified string of authors.
|
||||
* @example
|
||||
*
|
||||
* [Author1, Author2] becomes 'Author1 and Author2'
|
||||
* [Author1, Author2, Author3] becomes 'Author1, Author2, and Author3'
|
||||
*
|
||||
*/
|
||||
export const formatAuthors = (
|
||||
authors: NonNullable<NonNullable<Post['populatedAuthors']>[number]>[]
|
||||
) => {
|
||||
// Ensure we don't have any authors without a name
|
||||
const authorNames = authors.map((author) => author.name).filter(Boolean)
|
||||
|
||||
if (authorNames.length === 0) return ''
|
||||
if (authorNames.length === 1) return authorNames[0]
|
||||
if (authorNames.length === 2) return `${authorNames[0]} and ${authorNames[1]}`
|
||||
|
||||
return `${authorNames.slice(0, -1).join(', ')} and ${authorNames[authorNames.length - 1]}`
|
||||
}
|
||||
@@ -2,8 +2,8 @@ import type { Metadata } from 'next'
|
||||
|
||||
import type { Media, Post, Config } from '../payload-types'
|
||||
|
||||
import { mergeOpenGraph } from './mergeOpenGraph'
|
||||
import { getServerSideURL } from './getURL'
|
||||
import { mergeOpenGraph } from './mergeOpenGraph.ts'
|
||||
import { getServerSideURL } from './getURL.ts'
|
||||
|
||||
const getImageURL = (image?: Media | Config['db']['defaultIDType'] | null) => {
|
||||
const serverUrl = getServerSideURL()
|
||||
|
||||
@@ -2,7 +2,6 @@ import { PayloadRequest, CollectionSlug } from 'payload'
|
||||
|
||||
const collectionPrefixMap: Partial<Record<CollectionSlug, string>> = {
|
||||
posts: '/blog',
|
||||
pages: '',
|
||||
}
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Config } from 'src/payload-types'
|
||||
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
import { unstable_cache } from 'next/cache'
|
||||
// Do not import next/cache at module scope to keep migrate step Node-compatible
|
||||
|
||||
type Collection = keyof Config['collections']
|
||||
|
||||
@@ -25,7 +25,16 @@ async function getDocument(collection: Collection, slug: string, depth = 0) {
|
||||
/**
|
||||
* Returns a unstable_cache function mapped with the cache tag for the slug
|
||||
*/
|
||||
export const getCachedDocument = (collection: Collection, slug: string) =>
|
||||
unstable_cache(async () => getDocument(collection, slug), [collection, slug], {
|
||||
tags: [`${collection}_${slug}`],
|
||||
})
|
||||
export const getCachedDocument = (collection: Collection, slug: string) => {
|
||||
return async () => {
|
||||
try {
|
||||
const { unstable_cache } = await import('next/cache')
|
||||
const cached = unstable_cache(async () => getDocument(collection, slug), [collection, slug], {
|
||||
tags: [`${collection}_${slug}`],
|
||||
})
|
||||
return cached()
|
||||
} catch {
|
||||
return getDocument(collection, slug)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import type { Config } from 'src/payload-types'
|
||||
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
import { unstable_cache } from 'next/cache'
|
||||
// Do not import next/cache at module scope to keep migrate step Node-compatible
|
||||
|
||||
type Global = keyof Config['globals']
|
||||
|
||||
@@ -20,7 +20,16 @@ async function getGlobal(slug: Global, depth = 0) {
|
||||
/**
|
||||
* Returns a unstable_cache function mapped with the cache tag for the slug
|
||||
*/
|
||||
export const getCachedGlobal = (slug: Global, depth = 0) =>
|
||||
unstable_cache(async () => getGlobal(slug, depth), [slug], {
|
||||
tags: [`global_${slug}`],
|
||||
})
|
||||
export const getCachedGlobal = (slug: Global, depth = 0) => {
|
||||
return async () => {
|
||||
try {
|
||||
const { unstable_cache } = await import('next/cache')
|
||||
const cached = unstable_cache(async () => getGlobal(slug, depth), [slug], {
|
||||
tags: [`global_${slug}`],
|
||||
})
|
||||
return cached()
|
||||
} catch {
|
||||
return getGlobal(slug, depth)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { cookies } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
import type { User } from '../payload-types'
|
||||
import { getClientSideURL } from './getURL'
|
||||
import { getClientSideURL } from './getURL.ts'
|
||||
|
||||
export const getMeUser = async (args?: {
|
||||
nullUserRedirect?: string
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
import { unstable_cache } from 'next/cache'
|
||||
|
||||
export async function getRedirects(depth = 1) {
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
const { docs: redirects } = await payload.find({
|
||||
collection: 'redirects',
|
||||
depth,
|
||||
limit: 0,
|
||||
pagination: false,
|
||||
})
|
||||
|
||||
return redirects
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a unstable_cache function mapped with the cache tag for 'redirects'.
|
||||
*
|
||||
* Cache all redirects together to avoid multiple fetches.
|
||||
*/
|
||||
export const getCachedRedirects = () =>
|
||||
unstable_cache(async () => getRedirects(), ['redirects'], {
|
||||
tags: ['redirects'],
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import canUseDOM from './canUseDOM'
|
||||
import canUseDOM from './canUseDOM.ts'
|
||||
|
||||
export const getServerSideURL = () => {
|
||||
let url = process.env.NEXT_PUBLIC_SERVER_URL
|
||||
@@ -8,7 +8,7 @@ export const getServerSideURL = () => {
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
url = 'http://localhost:3000'
|
||||
url = 'http://localhost:3030'
|
||||
}
|
||||
|
||||
return url
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { getServerSideURL } from './getURL'
|
||||
import { getServerSideURL } from './getURL.ts'
|
||||
|
||||
const defaultOpenGraph: Metadata['openGraph'] = {
|
||||
type: 'website',
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
# https://supabase.com/docs/guides/local-development/cli/config
|
||||
# A string used to distinguish different Supabase projects on the same host. Defaults to the
|
||||
# working directory name when running `supabase init`.
|
||||
project_id = "cms-payload"
|
||||
project_id = "cms"
|
||||
|
||||
[api]
|
||||
enabled = true
|
||||
# Port to use for the API URL.
|
||||
port = 54321
|
||||
port = 34321
|
||||
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
|
||||
# endpoints. `public` and `graphql_public` schemas are included by default.
|
||||
schemas = ["public", "graphql_public"]
|
||||
@@ -23,17 +23,17 @@ enabled = false
|
||||
|
||||
[db]
|
||||
# Port to use for the local database URL.
|
||||
port = 54322
|
||||
port = 34322
|
||||
# Port used by db diff command to initialize the shadow database.
|
||||
shadow_port = 54320
|
||||
shadow_port = 34320
|
||||
# The database major version to use. This has to be the same as your remote database's. Run `SHOW
|
||||
# server_version;` on the remote database to check.
|
||||
major_version = 15
|
||||
major_version = 17
|
||||
|
||||
[db.pooler]
|
||||
enabled = false
|
||||
# Port to use for the local connection pooler.
|
||||
port = 54329
|
||||
port = 34329
|
||||
# Specifies when a server connection can be reused by other clients.
|
||||
# Configure one of the supported pooler modes: `transaction`, `session`.
|
||||
pool_mode = "transaction"
|
||||
@@ -46,6 +46,8 @@ max_client_conn = 100
|
||||
# secret_key = "env(SECRET_VALUE)"
|
||||
|
||||
[db.migrations]
|
||||
# If disabled, migrations will be skipped during a db push or reset.
|
||||
enabled = true
|
||||
# Specifies an ordered list of schema files that describe your database.
|
||||
# Supports glob patterns relative to supabase directory: "./schemas/*.sql"
|
||||
schema_paths = []
|
||||
@@ -57,6 +59,16 @@ enabled = true
|
||||
# Supports glob patterns relative to supabase directory: "./seeds/*.sql"
|
||||
sql_paths = ["./seed.sql"]
|
||||
|
||||
[db.network_restrictions]
|
||||
# Enable management of network restrictions.
|
||||
enabled = false
|
||||
# List of IPv4 CIDR blocks allowed to connect to the database.
|
||||
# Defaults to allow all IPv4 connections. Set empty array to block all IPs.
|
||||
allowed_cidrs = ["0.0.0.0/0"]
|
||||
# List of IPv6 CIDR blocks allowed to connect to the database.
|
||||
# Defaults to allow all IPv6 connections. Set empty array to block all IPs.
|
||||
allowed_cidrs_v6 = ["::/0"]
|
||||
|
||||
[realtime]
|
||||
enabled = true
|
||||
# Bind realtime via either IPv4 or IPv6. (default: IPv4)
|
||||
@@ -67,7 +79,7 @@ enabled = true
|
||||
[studio]
|
||||
enabled = true
|
||||
# Port to use for Supabase Studio.
|
||||
port = 54323
|
||||
port = 34323
|
||||
# External URL of the API server that frontend connects to.
|
||||
api_url = "http://127.0.0.1"
|
||||
# OpenAI API Key to use for Supabase AI in the Supabase Studio.
|
||||
@@ -78,10 +90,10 @@ openai_api_key = "env(OPENAI_API_KEY)"
|
||||
[inbucket]
|
||||
enabled = true
|
||||
# Port to use for the email testing server web interface.
|
||||
port = 54324
|
||||
port = 34324
|
||||
# Uncomment to expose additional ports for testing user applications that send emails.
|
||||
# smtp_port = 54325
|
||||
# pop3_port = 54326
|
||||
# smtp_port = 34325
|
||||
# pop3_port = 34326
|
||||
# admin_email = "admin@email.com"
|
||||
# sender_name = "Admin"
|
||||
|
||||
@@ -110,6 +122,8 @@ site_url = "http://127.0.0.1:3000"
|
||||
additional_redirect_urls = ["https://127.0.0.1:3000"]
|
||||
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
|
||||
jwt_expiry = 3600
|
||||
# Path to JWT signing key. DO NOT commit your signing keys file to git.
|
||||
# signing_keys_path = "./signing_keys.json"
|
||||
# If disabled, the refresh token will never expire.
|
||||
enable_refresh_token_rotation = true
|
||||
# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
|
||||
@@ -140,6 +154,8 @@ token_refresh = 150
|
||||
sign_in_sign_ups = 30
|
||||
# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address.
|
||||
token_verifications = 30
|
||||
# Number of Web3 logins that can be made in a 5 minute interval per IP address.
|
||||
web3 = 30
|
||||
|
||||
# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`.
|
||||
# [auth.captcha]
|
||||
@@ -200,6 +216,11 @@ max_frequency = "5s"
|
||||
# Force log out if the user has been inactive longer than the specified duration.
|
||||
# inactivity_timeout = "8h"
|
||||
|
||||
# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object.
|
||||
# [auth.hook.before_user_created]
|
||||
# enabled = true
|
||||
# uri = "pg-functions://postgres/auth/before-user-created-hook"
|
||||
|
||||
# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
|
||||
# [auth.hook.custom_access_token]
|
||||
# enabled = true
|
||||
@@ -252,6 +273,11 @@ url = ""
|
||||
# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
|
||||
skip_nonce_check = false
|
||||
|
||||
# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard.
|
||||
# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting.
|
||||
[auth.web3.solana]
|
||||
enabled = false
|
||||
|
||||
# Use Firebase Auth as a third-party provider alongside Supabase Auth.
|
||||
[auth.third_party.firebase]
|
||||
enabled = false
|
||||
@@ -276,7 +302,7 @@ enabled = false
|
||||
# domain = "example.clerk.accounts.dev"
|
||||
|
||||
[edge_runtime]
|
||||
enabled = true
|
||||
enabled = false
|
||||
# Configure one of the supported request policies: `oneshot`, `per_worker`.
|
||||
# Use `oneshot` for hot reload, or `per_worker` for load testing.
|
||||
policy = "oneshot"
|
||||
@@ -289,8 +315,8 @@ deno_version = 1
|
||||
# secret_key = "env(SECRET_VALUE)"
|
||||
|
||||
[analytics]
|
||||
enabled = true
|
||||
port = 54327
|
||||
enabled = false
|
||||
port = 34327
|
||||
# Configure one of the supported backends: `postgres`, `bigquery`.
|
||||
backend = "postgres"
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Strictness */
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
"baseUrl": ".",
|
||||
"esModuleInterop": true,
|
||||
"target": "ES2022",
|
||||
@@ -18,6 +16,7 @@
|
||||
"jsx": "preserve",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"sourceMap": true,
|
||||
"isolatedModules": true,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||
"buildCommand": "pnpm vercel-build",
|
||||
"installCommand": "pnpm install"
|
||||
}
|
||||
@@ -44,7 +44,7 @@
|
||||
"ui": "workspace:*",
|
||||
"ui-patterns": "workspace:*",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"zod": "^3.22.4"
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@shikijs/compat": "^1.1.7",
|
||||
|
||||
@@ -2477,11 +2477,12 @@ export const deployment: NavMenuConstant = {
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Overview', url: '/guides/deployment/branching' },
|
||||
{ name: 'GitHub integration', url: '/guides/deployment/branching/github-integration' },
|
||||
{ name: 'Branching via GitHub', url: '/guides/deployment/branching/github-integration' },
|
||||
{
|
||||
name: 'Branching 2.0 (Alpha)',
|
||||
url: '/guides/deployment/branching/branching-2',
|
||||
name: 'Branching via dashboard',
|
||||
url: '/guides/deployment/branching/dashboard',
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Working with branches',
|
||||
url: '/guides/deployment/branching/working-with-branches',
|
||||
|
||||
@@ -22,15 +22,13 @@ Now set up the database schema. You can use the "User Management Starter" quicks
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to the [SQL Editor](https://supabase.com/dashboard/project/_/sql) page in the Dashboard.
|
||||
2. Click **User Management Starter**.
|
||||
2. Click **User Management Starter** under the **Community > Quickstarts** tab.
|
||||
3. Click **Run**.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
You can pull the database schema down to your local project by running the `db pull` command. Read the [local development docs](/docs/guides/cli/local-development#link-your-project) for detailed instructions.
|
||||
|
||||
{/* TODO: discuss */}
|
||||
|
||||
```bash
|
||||
supabase link --project-ref <project-id>
|
||||
# You can get <project-id> from your project's dashboard URL: https://supabase.com/dashboard/project/<project-id>
|
||||
|
||||
@@ -2,4 +2,4 @@ This tutorial demonstrates how to build a basic user management app. The app aut
|
||||
|
||||
- [Supabase Database](/docs/guides/database) - a Postgres database for storing your user data and [Row Level Security](/docs/guides/auth#row-level-security) so data is protected and users can only access their own information.
|
||||
- [Supabase Auth](/docs/guides/auth) - allow users to sign up and log in.
|
||||
- [Supabase Storage](/docs/guides/storage) - users can upload a profile photo.
|
||||
- [Supabase Storage](/docs/guides/storage) - allow users to upload a profile photo.
|
||||
@@ -95,7 +95,7 @@ To use Google's pre-built signin buttons:
|
||||
|
||||
1. Configure OAuth credentials for your Google Cloud project in the [Credentials](https://console.cloud.google.com/apis/credentials) page of the console. When creating a new OAuth client ID, choose _Android_ or _iOS_ depending on the mobile operating system your app is built for.
|
||||
- For Android, use the instructions on screen to provide the SHA-1 certificate fingerprint used to sign your Android app.
|
||||
- You will have a different set of SHA-1 certificate fingerprint for testing locally and going to production. Make sure to add both to the Google Cloud Console. and add all of the Client IDs to Supabase dashboard.
|
||||
- You will have a different set of SHA-1 certificate fingerprints for testing locally and going to production. Make sure to add both to the Google Cloud Console, and add all of the Client IDs to the Supabase dashboard.
|
||||
- For iOS, use the instructions on screen to provide the app Bundle ID, and App Store ID and Team ID if the app is already published on the Apple App Store.
|
||||
2. Configure the [OAuth Consent Screen](https://console.cloud.google.com/apis/credentials/consent). This information is shown to the user when giving consent to your app. In particular, make sure you have set up links to your app's privacy policy and terms of service.
|
||||
3. Finally, add the client ID from step 1 in the [Google provider on the Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/providers), under _Client IDs_.
|
||||
|
||||
@@ -188,4 +188,4 @@ Do reach out to support if you have any concerns around this change.
|
||||
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [LinkedIn Developer Dashboard](https://api.LinkedIn.com/apps)
|
||||
- [LinkedIn Developer Dashboard](https://www.linkedin.com/developers/apps)
|
||||
+1
-1
@@ -26,7 +26,7 @@ This is made possible if the providers are using JWTs signed with asymmetric key
|
||||
|
||||
There are some limitations you should be aware of when using third-party authentication providers with Supabase.
|
||||
|
||||
1. The third-party provider must use asymmetrically signed JWTs (exposed as an OIDC Issuer Discovery URL by the third-party authentication provider). Using symmetrically signed JWTs is not possible at this time.
|
||||
1. The third-party provider must use asymmetrically signed JWTs (exposed as an OIDC Issuer Discovery URL by the third-party authentication provider). The signed JWTs must have a `kid` header parameter to identify which key must be used. Using symmetrically signed JWTs is not possible at this time.
|
||||
2. The JWT signing keys from the third-party provider are stored in the configuration of your project, and are checked for changes periodically. If you are rotating your keys (when supported) allow up to 30 minutes for the change to be picked up.
|
||||
3. It is not possible to disable Supabase Auth at this time.
|
||||
|
||||
|
||||
@@ -12,5 +12,5 @@ Supabase branches create separate environments that spin off from your main proj
|
||||
- **Separate Environments**: Each branch is a separate environment with its own Supabase instance and API credentials.
|
||||
- **Preview Branches**: You can create multiple Preview Branches for testing.
|
||||
- **Persistent Branches**: Persistent branches are long-lived branches. They aren't automatically paused or deleted due to non-inactivity or merging.
|
||||
- **GitHub integration and Branching 2.0**: You can use branching with the GitHub integration, or use Branching 2.0 to manage your branches without Git (in Feature Preview).
|
||||
- **Managing Branches**: You can create, review, and merge branches either automatically via our [GitHub integration](/docs/guides/deployment/branching/github-integration) or directly [through the dashboard](/docs/guides/deployment/branching/dashboard) (currently in beta). All branches show up in the branches page in the dashboard, regardless of how they were created.
|
||||
- **Data-less**: New branches do not start with any data from your main project. This is meant to better protect your sensitive production data. To start your branches with data, you can use a [seed file](/docs/guides/deployment/branching/github-integration#seeding) if using the GitHub integration.
|
||||
+10
-10
@@ -1,36 +1,36 @@
|
||||
---
|
||||
title: 'Branching 2.0 (Alpha)'
|
||||
subtitle: 'Create and manage branches without using Git'
|
||||
title: 'Branching via the dashboard'
|
||||
subtitle: 'Create, manage, review, and merge branches directly in the dashboard'
|
||||
---
|
||||
|
||||
Branching 2.0 allows you to create and manage Supabase branches without connecting to a Git repository. This is useful for quick testing, prototyping, or when you prefer to manage your database changes outside of Git.
|
||||
You can create, manage, review, and merge Supabase branches directly via the dashboard. This is useful for quick testing, prototyping, or when you prefer to work in a no-code way. You can also connect a Supabase branch to a GitHub branch at a later time if needed.
|
||||
|
||||
<Admonition type="note" label="Public Alpha">
|
||||
|
||||
Branching 2.0 is currently in public alpha. Features and functionality may change.
|
||||
Branch management via the dashboard is currently in public alpha. Features and functionality may change.
|
||||
|
||||
</Admonition>
|
||||
|
||||
## How Branching 2.0 works
|
||||
## How Branching works
|
||||
|
||||
With Branching 2.0, you can do the following directly from the Supabase dashboard:
|
||||
You can do the following directly from the Supabase dashboard:
|
||||
|
||||
- Create preview branches
|
||||
- Make changes to your public schema or edge functions
|
||||
- Merge these changes back into production when ready
|
||||
- Pull in updates from production
|
||||
|
||||
## Enable Branching 2.0
|
||||
## Enable branch management via the dashboard
|
||||
|
||||
Branching 2.0 is available as a Feature Preview. To enable Branching 2.0 in the Supabase Dashboard:
|
||||
This functionality is currently in beta and requires opting in. To opt in you must enable the feature preview:
|
||||
|
||||
1. Open the user menu by clicking on your user icon in the top right.
|
||||
1. Select **Branching 2.0**.
|
||||
1. Select **Branching via dashboard**.
|
||||
1. Click **Enable feature**.
|
||||
|
||||
## Creating a branch
|
||||
|
||||
Once you've enabled Branching 2.0, you can create a new branch:
|
||||
Once you've enabled the feature, you can create a new branch:
|
||||
|
||||
1. Click the arrows next to the branch name in the top menu bar. (The top menu bar has the format `YOUR_ORGANIZATION / YOUR_PROJECT / CURRENT_BRANCH_NAME`.)
|
||||
2. Click `Create branch`.
|
||||
@@ -48,6 +48,9 @@ jobs:
|
||||
- wait
|
||||
if: ${{ needs.wait.outputs.status == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
|
||||
SUPABASE_PROJECT_ID: ${{ secrets.SUPABASE_PROJECT_ID }}
|
||||
steps:
|
||||
- uses: supabase/setup-cli@v1
|
||||
with:
|
||||
|
||||
@@ -18,8 +18,8 @@ After developing your project and deciding it's Production Ready, you should run
|
||||
- Enable replication on tables containing sensitive data by enabling Row Level Security (RLS) and setting row security policies:
|
||||
- Go to the Authentication > Policies page in the Supabase Dashboard to enable RLS and create security policies.
|
||||
- Go to the Database > Publications page in the Supabase Dashboard to manage replication tables.
|
||||
- Turn on [SSL Enforcement](/docs/guides/platform/ssl-enforcement)
|
||||
- Enable [Network Restrictions](/docs/guides/platform/network-restrictions) for your database.
|
||||
- Turn on [SSL Enforcement](/docs/guides/platform/ssl-enforcement) (see: [dashboard](https://supabase.com/dashboard/project/_/auth/policies))
|
||||
- Enable [Network Restrictions](/docs/guides/platform/network-restrictions) for your database (see: [dashboard](https://supabase.com/dashboard/project/_/database/settings#network-restrictions)).
|
||||
- Ensure that your Supabase Account is protected with multi-factor authentication (MFA).
|
||||
- If using a GitHub signin, [enable 2FA on GitHub](https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa/configuring-two-factor-authentication). Since your GitHub account gives you administrative rights to your Supabase org, you should protect it with a strong password and 2FA using a U2F key or a TOTP app.
|
||||
- If using email+password signin, set up [MFA for your Supabase account](https://supabase.com/docs/guides/platform/multi-factor-authentication#enable-mfa).
|
||||
|
||||
@@ -31,4 +31,5 @@ subtitle: "Limits applied Edge Functions in Supabase's hosted platform."
|
||||
- Outgoing connections to ports `25` and `587` are not allowed.
|
||||
- Serving of HTML content is only supported with [custom domains](/docs/reference/cli/supabase-domains) (Otherwise `GET` requests that return `text/html` will be rewritten to `text/plain`).
|
||||
- Web Worker API (or Node `vm` API) are not available.
|
||||
- Static files cannot be deployed using the API flag. You need to build them with [Docker on the CLI](/docs/guides/functions/quickstart#step-6-deploy-to-production).
|
||||
- Node Libraries that require multithreading are not supported. Examples: [`libvips`](https://github.com/libvips/libvips), [sharp](https://github.com/lovell/sharp).
|
||||
@@ -122,7 +122,8 @@ Before deploying, ensure the Wasm module is bundled with your function by defini
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
You will need update Supabase CLI to 2.7.0 or higher for the `static_files` support.
|
||||
- You will need update Supabase CLI to 2.7.0 or higher for the `static_files` support.
|
||||
- Static files cannot be deployed using the `--use-api` API flag. You need to build them with [Docker on the CLI](/docs/guides/functions/quickstart#step-6-deploy-to-production).
|
||||
|
||||
</Admonition>
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@ If you get stuck while working through this guide, refer to the [full example on
|
||||
|
||||
## Building the app
|
||||
|
||||
Let's start building the Next.js app from scratch.
|
||||
Start building the Next.js app from scratch.
|
||||
|
||||
### Initialize a Next.js app
|
||||
|
||||
We can use [`create-next-app`](https://nextjs.org/docs/getting-started) to initialize an app called `supabase-nextjs`:
|
||||
Use [`create-next-app`](https://nextjs.org/docs/getting-started) to initialize an app called `supabase-nextjs`:
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -54,8 +54,7 @@ Then install the Supabase client library: [supabase-js](https://github.com/supab
|
||||
npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
And finally we want to save the environment variables in a `.env.local`.
|
||||
Create a `.env.local` file at the root of the project, and paste the API URL and the `anon` key that you copied [earlier](#get-the-api-keys).
|
||||
Save the environment variables in a `.env.local` file at the root of the project, and paste the API URL and the `anon` key that you copied [earlier](#get-the-api-keys).
|
||||
|
||||
```bash .env.local
|
||||
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
|
||||
@@ -65,13 +64,13 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
|
||||
### App styling (optional)
|
||||
|
||||
An optional step is to update the CSS file `app/globals.css` to make the app look nice.
|
||||
You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/nextjs-user-management/app/globals.css).
|
||||
You can find the full contents of this file [in the example repository](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/nextjs-user-management/app/globals.css).
|
||||
|
||||
### Supabase Server-Side Auth
|
||||
|
||||
Next.js is a highly versatile framework offering pre-rendering at build time (SSG), server-side rendering at request time (SSR), API routes, and middleware edge-functions.
|
||||
|
||||
To better integrate with the framework, we've created the `@supabase/ssr` package for Server-Side Auth. It has all the functionalities to quickly configure your Supabase project to use cookies for storing user sessions. See the [Next.js Server-Side Auth guide](/docs/guides/auth/server-side/nextjs) for more information.
|
||||
To better integrate with the framework, we've created the `@supabase/ssr` package for Server-Side Auth. It has all the functionalities to quickly configure your Supabase project to use cookies for storing user sessions. Read the [Next.js Server-Side Auth guide](/docs/guides/auth/server-side/nextjs) for more information.
|
||||
|
||||
Install the package for Next.js.
|
||||
|
||||
@@ -184,11 +183,11 @@ Since Server Components can't write cookies, you need middleware to refresh expi
|
||||
- Passing the refreshed Auth token to Server Components through `request.cookies.set`, so they don't attempt to refresh the same token themselves.
|
||||
- Passing the refreshed Auth token to the browser, so it replaces the old token. This is done with `response.cookies.set`.
|
||||
|
||||
You could also add a matcher, so that the middleware only runs on route that access Supabase. For more information, check out this [documentation](https://nextjs.org/docs/app/building-your-application/routing/middleware#matching-paths).
|
||||
You could also add a matcher, so that the middleware only runs on routes that access Supabase. For more information, read [the Next.js matcher documentation](https://nextjs.org/docs/app/api-reference/file-conventions/middleware#matcher).
|
||||
|
||||
<Admonition type="danger">
|
||||
|
||||
Be careful when protecting pages. The server gets the user session from the cookies, which can be spoofed by anyone.
|
||||
Be careful when protecting pages. The server gets the user session from the cookies, which anyone can spoof.
|
||||
|
||||
Always use `supabase.auth.getUser()` to protect pages and user data.
|
||||
|
||||
@@ -358,17 +357,17 @@ meta="name=app/login/page.tsx"
|
||||
|
||||
</Tabs>
|
||||
|
||||
Navigate to `http://localhost:3000/login`. You should see your login form, but it's not yet hooked up to the actual login function. Next, you need to create the login/signup actions. They will:
|
||||
Next, you need to create the login/signup actions to hook up the form to the function. Which does the following:
|
||||
|
||||
- Retrieve the user's information.
|
||||
- Send that information to Supabase as a signup request, which in turns will send a confirmation email.
|
||||
- Send that information to Supabase as a signup request, which in turns sends a confirmation email.
|
||||
- Handle any error that arises.
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
Note that cookies is called before any calls to Supabase, which opts fetch calls out of Next.js's caching. This is important for authenticated data fetches, to ensure that users get access only to their own data.
|
||||
The `cookies` method is called before any calls to Supabase, which takes fetch calls out of Next.js's caching. This is important for authenticated data fetches, to ensure that users get access only to their own data.
|
||||
|
||||
See the Next.js docs to learn more about [opting out of data caching](https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#opting-out-of-data-caching).
|
||||
Read the Next.js docs to learn more about [opting out of data caching](https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#opting-out-of-data-caching).
|
||||
|
||||
</Admonition>
|
||||
|
||||
@@ -382,6 +381,8 @@ See the Next.js docs to learn more about [opting out of data caching](https://ne
|
||||
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
Create the `action.js` file in the `app/login` folder, which contains the login and signup functions and the `error/page.jsx` file, and displays an error message if the login or signup fails.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```js name=app/login/actions.js
|
||||
@@ -409,7 +410,6 @@ export async function login(formData) {
|
||||
}
|
||||
|
||||
revalidatePath('/', 'layout')
|
||||
redirect('/account')
|
||||
}
|
||||
|
||||
export async function signup(formData) {
|
||||
@@ -427,7 +427,6 @@ export async function signup(formData) {
|
||||
}
|
||||
|
||||
revalidatePath('/', 'layout')
|
||||
redirect('/account')
|
||||
}
|
||||
```
|
||||
|
||||
@@ -443,6 +442,8 @@ export default function ErrorPage() {
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
Create the `action.ts` file in the `app/login` folder, which contains the login and signup functions and the `error/page.tsx` file, which displays an error message if the login or signup fails.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
<$CodeSample
|
||||
@@ -461,35 +462,31 @@ meta="name=app/error/page.tsx"
|
||||
|
||||
</TabPanel>
|
||||
|
||||
When you enter your email and password, you will receive an email with the title **Confirm Your Signup**. Congrats 🎉!!!
|
||||
|
||||
</Tabs>
|
||||
|
||||
### Email template
|
||||
|
||||
Change the email template to support a server-side authentication flow.
|
||||
|
||||
Before we proceed, let's change the email template to support sending a token hash:
|
||||
Before proceeding, change the email template to support support a server-side authentication flow that sends a token hash:
|
||||
|
||||
- Go to the [Auth templates](/dashboard/project/_/auth/templates) page in your dashboard.
|
||||
- Select `Confirm signup` template.
|
||||
- Select the **Confirm signup** template.
|
||||
- Change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email`.
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
Did you know? You could also customize emails sent out to new users, including the email's looks, content, and query parameters. Check out the [settings of your project](/dashboard/project/_/auth/templates).
|
||||
**Did you know?** You can also customize other emails sent out to new users, including the email's looks, content, and query parameters. Check out the [settings of your project](/dashboard/project/_/auth/templates).
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Confirmation endpoint
|
||||
|
||||
As we are working in a server-side rendering (SSR) environment, it is necessary to create a server endpoint responsible for exchanging the `token_hash` for a session.
|
||||
As you are working in a server-side rendering (SSR) environment, you need to create a server endpoint responsible for exchanging the `token_hash` for a session.
|
||||
|
||||
In the following code snippet, we perform the following steps:
|
||||
The code performs the following steps:
|
||||
|
||||
- Retrieve the code sent back from the Supabase Auth server using the `token_hash` query parameter.
|
||||
- Exchange this code for a session, which we store in our chosen storage mechanism (in this case, cookies).
|
||||
- Finally, we redirect the user to the `account` page.
|
||||
- Retrieves the code sent back from the Supabase Auth server using the `token_hash` query parameter.
|
||||
- Exchanges this code for a session, which you store in your chosen storage mechanism (in this case, cookies).
|
||||
- Finally, redirects the user to the `account` page.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -505,7 +502,6 @@ In the following code snippet, we perform the following steps:
|
||||
|
||||
```js name=app/auth/confirm/route.js
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
import { createClient } from '@/utils/supabase/server'
|
||||
|
||||
// Creating a handler to a GET request to route /auth/confirm
|
||||
@@ -562,9 +558,9 @@ meta="name=app/auth/confirm/route.ts"
|
||||
|
||||
### Account page
|
||||
|
||||
After a user is signed in we can allow them to edit their profile details and manage their account.
|
||||
After a user signs in, allow them to edit their profile details and manage their account.
|
||||
|
||||
Let's create a new component for that called `AccountForm` within the `app/account` folder.
|
||||
Create a new component for that called `AccountForm` within the `app/account` folder.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -589,7 +585,6 @@ export default function AccountForm({ user }) {
|
||||
const [fullname, setFullname] = useState(null)
|
||||
const [username, setUsername] = useState(null)
|
||||
const [website, setWebsite] = useState(null)
|
||||
const [avatar_url, setAvatarUrl] = useState(null)
|
||||
|
||||
const getProfile = useCallback(async () => {
|
||||
try {
|
||||
@@ -609,7 +604,6 @@ export default function AccountForm({ user }) {
|
||||
setFullname(data.full_name)
|
||||
setUsername(data.username)
|
||||
setWebsite(data.website)
|
||||
setAvatarUrl(data.avatar_url)
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error loading user data!')
|
||||
@@ -631,7 +625,6 @@ export default function AccountForm({ user }) {
|
||||
full_name: fullname,
|
||||
username,
|
||||
website,
|
||||
avatar_url,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
if (error) throw error
|
||||
@@ -680,7 +673,7 @@ export default function AccountForm({ user }) {
|
||||
<div>
|
||||
<button
|
||||
className="button primary block"
|
||||
onClick={() => updateProfile({ fullname, username, website, avatar_url })}
|
||||
onClick={() => updateProfile({ fullname, username, website })}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Loading ...' : 'Update'}
|
||||
@@ -719,7 +712,7 @@ meta="name=app/account/account-form.tsx"
|
||||
|
||||
</Tabs>
|
||||
|
||||
Create an account page for the `AccountForm` component we just created
|
||||
Create an account page for the `AccountForm` component you just created
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -769,7 +762,7 @@ meta="name=app/account/page.tsx"
|
||||
|
||||
### Sign out
|
||||
|
||||
Let's create a route handler to handle the signout from the server side. Make sure to check if the user is logged in first!
|
||||
Create a route handler to handle the sign out from the server side, making sure to check if the user is logged in first.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -829,13 +822,15 @@ meta="name=app/auth/signout/route.ts"
|
||||
|
||||
### Launch!
|
||||
|
||||
Now that we have all the pages, route handlers and components in place, let's run this in a terminal window:
|
||||
Now you have all the pages, route handlers, and components in place, run the following in a terminal window:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
And then open the browser to [localhost:3000](http://localhost:3000) and you should see the completed app.
|
||||
And then open the browser to [localhost:3000/login](http://localhost:3000/login) and you should see the completed app.
|
||||
|
||||
When you enter your email and password, you will receive an email with the title **Confirm Your Signup**. Congrats 🎉!!!
|
||||
|
||||
## Bonus: Profile photos
|
||||
|
||||
@@ -844,7 +839,7 @@ photos and videos.
|
||||
|
||||
### Create an upload widget
|
||||
|
||||
Let's create an avatar widget for the user so that they can upload a profile photo. We can start by creating a new component:
|
||||
Create an avatar widget for the user so that they can upload a profile photo. Start by creating a new component:
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -970,7 +965,7 @@ meta="name=app/account/avatar.tsx"
|
||||
|
||||
### Add the new widget
|
||||
|
||||
And then we can add the widget to the `AccountForm` component:
|
||||
Then add the widget to the `AccountForm` component:
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
|
||||
Loaded 100 of 320 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user