Merge branch 'master' into feature/database-tests

This commit is contained in:
Saxon Fletcher committed 2025-08-18 10:11:34 +10:00
commit 4ee87e8a0e
636 files changed
+40476 -19831

No files matched your search

+4 -1
View File
@@ -45,6 +45,7 @@ jobs:
sparse-checkout: |
apps/docs
apps/www/.env.local.example
examples
supabase
- uses: pnpm/action-setup@v4
@@ -63,7 +64,9 @@ jobs:
- name: Update embeddings
working-directory: ./apps/docs
if: ${{ !inputs.refresh }}
run: pnpm run embeddings
run: |
pnpm run codegen:examples
pnpm run embeddings
- name: Refresh embeddings
working-directory: ./apps/docs
+17
View File
@@ -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
-13
View File
@@ -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
View File
@@ -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
+48
View File
@@ -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
+1 -1
View File
@@ -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
+18 -1
View File
@@ -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
View File
@@ -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"
}
}
-20
View File
@@ -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
+28
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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',
},
}
+1 -1
View File
@@ -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()
-43
View File
@@ -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>
)
}
-79
View File
@@ -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>
)
}
-162
View File
@@ -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>
)
}
-982
View File
@@ -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',
},
]
-38
View File
@@ -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>
)
}
-15
View File
@@ -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>
)
}
-36
View File
@@ -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>
)
}
-63
View File
@@ -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>
)
}
-64
View File
@@ -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>
)
}
-52
View File
@@ -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' },
]
-32
View File
@@ -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>
)
}
-13
View File
@@ -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>
)
}
-51
View File
@@ -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',
},
}
-21
View File
@@ -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>
)
}
-51
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
import { CollectionConfig } from 'payload'
import type { CollectionConfig } from 'payload'
export const Authors: CollectionConfig = {
slug: 'authors',
+1 -1
View File
@@ -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 -9
View File
@@ -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
+10 -10
View File
@@ -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' },
+2 -2
View File
@@ -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
+10 -10
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
import { CollectionConfig } from 'payload'
import type { CollectionConfig } from 'payload'
export const Tags: CollectionConfig = {
slug: 'tags',
+2 -2
View File
@@ -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',
+9 -5
View File
@@ -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>, &nbsp;</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>
+4 -6
View File
@@ -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()
}
+1 -9
View File
@@ -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} />,
},
+2 -2
View File
@@ -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,
},
{
+3 -3
View File
@@ -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 -1
View File
@@ -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
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;`)
}
+16 -4
View File
@@ -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'
},
];
+14
View File
@@ -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
+71 -16
View File
@@ -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,
+11
View File
@@ -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 || '',
},
}),
})
-87
View File
@@ -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,
},
}),
]
+5 -1
View File
@@ -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)
+1 -1
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
import type { Theme } from './types'
import type { Theme } from './types.ts'
export const themeLocalStorageKey = 'payload-theme'
-41
View File
@@ -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>
)
}
-44
View File
@@ -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
}
-61
View File
@@ -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',
},
],
},
]
-24
View File
@@ -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 -2
View File
@@ -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 = {
+14 -5
View File
@@ -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)
}
}
}
+14 -5
View File
@@ -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)
}
}
}
+1 -1
View File
@@ -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
-26
View File
@@ -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'],
})
+2 -2
View File
@@ -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 -1
View File
@@ -1,5 +1,5 @@
import type { Metadata } from 'next'
import { getServerSideURL } from './getURL'
import { getServerSideURL } from './getURL.ts'
const defaultOpenGraph: Metadata['openGraph'] = {
type: 'website',
+39 -13
View File
@@ -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 -2
View File
@@ -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,
+5
View File
@@ -0,0 +1,5 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"buildCommand": "pnpm vercel-build",
"installCommand": "pnpm install"
}
+1 -1
View File
@@ -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",
+4 -4
View File
@@ -1,12 +1,12 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import './../../../packages/ui/build/css/source/global.css';
@import './../../../packages/ui/build/css/themes/dark.css';
@import './../../../packages/ui/build/css/themes/classic-dark.css';
@import './../../../packages/ui/build/css/themes/light.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--chart-1: 12 76% 61%;
@@ -4,7 +4,9 @@ import { type OpenAIClientInterface } from '~/lib/openAi'
import { ApiError } from '../../utils'
import { POST } from '../route'
const contentEmbeddingMock = vi.fn().mockImplementation(async () => Result.ok([0.1, 0.2, 0.3]))
const contentEmbeddingMock = vi
.fn()
.mockImplementation(async () => Result.ok({ embedding: [0.1, 0.2, 0.3], tokenCount: 10 }))
const openAIMock: OpenAIClientInterface = {
createContentEmbedding: contentEmbeddingMock,
}
+11 -1
View File
@@ -64,6 +64,16 @@ export class NoDataError<Details extends ObjectOrNever = never> extends ApiError
}
}
export class FileNotFoundError<Details extends ObjectOrNever = never> extends Error {
constructor(
message: string,
error: Error,
public details?: Details
) {
super(`FileNotFound: ${message}`, { cause: error })
}
}
export class MultiError<ErrorType = unknown, Details extends ObjectOrNever = never> extends Error {
constructor(
message: string,
@@ -79,7 +89,7 @@ export class MultiError<ErrorType = unknown, Details extends ObjectOrNever = nev
appendError(message: string, error: ErrorType): this {
this.message = `${this.message}\n\t${message}`
;((this.cause ?? (this.cause = [])) as Array<ErrorType>).push(error)
;((this.cause ??= []) as Array<ErrorType>).push(error)
return this
}
}
+22
View File
@@ -181,6 +181,8 @@ Line numbers are 1-indexed and inclusive.
lines={[[1, 3], [5, -1]]}
{/* Optional, displays as a file name on the code block */}
meta="name=display/path.js"
{/* Optional, strips TypeScript types to produce JavaScript, which you also include with another <CodeSample /> */}
convertToJs={true}
/>
```
@@ -195,11 +197,31 @@ commit="1623aa9b95ec90e21c5bae5a0d50dcf272abe92f"
path="/relative/path/from/root.js"
lines={[[1, 3], [5, -1]]}
meta="name=display/path.js"
convertToJs={true}
/>
```
The repo must be public, the org must be on the allow list, and the commit must be an immutable SHA (not a mutable tag or branch name).
#### Converting TypeScript to JavaScript
You can automatically strip TypeScript types from code samples to produce JavaScript using the `convertToJs` option:
```mdx
<$CodeSample
path="/path/to/typescript-file.ts"
lines={[[1, -1]]}
convertToJs={true}
/>
```
This is useful when you want to show JavaScript examples from TypeScript source files. The conversion:
- Removes all TypeScript type annotations, interfaces, and type definitions
- Converts the language identifier from `typescript` to `javascript` (or `tsx` to `jsx`)
- Happens before any line selection or elision processing
- Defaults to `false` to preserve TypeScript code when not specified
#### Multi-file code samples
Multi-file code samples use the `<$CodeTabs>` annotation:
@@ -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.
@@ -264,7 +264,8 @@ export async function updateSession(request: NextRequest) {
if (
!user &&
!request.nextUrl.pathname.startsWith('/login') &&
!request.nextUrl.pathname.startsWith('/auth')
!request.nextUrl.pathname.startsWith('/auth') &&
!request.nextUrl.pathname.startsWith('/error')
) {
// no user, potentially respond by redirecting the user to the login page
const url = request.nextUrl.clone()
@@ -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_.
@@ -397,7 +397,7 @@ const OneTapComponent = () => {
const supabase = createClient()
const router = useRouter()
const initializeGoogleOneTap = () => {
const initializeGoogleOneTap = async () => {
console.log('Initializing Google One Tap')
const [nonce, hashedNonce] = await generateNonce()
console.log('Nonce: ', nonce, hashedNonce)
@@ -801,4 +801,5 @@ chrome.identity.launchWebAuthFlow(
By default, the Google consent screen shows the root domain of the callback URL, where Google will send the authentication response. With Supabase Auth, it is your Supabase project's domain `(https://<your-project-ref>.supabase.co)`.
If that is not preferable, you can use a [Custom Domain](https://supabase.com/docs/guides/platform/custom-domains) with your Supabase project. You can use it as your project's domain when creating the Supabase client in your application and initiating the authentication flow. It will then show up in the Google consent screen. If you want your app name and the logo on the consent screen, [you must submit your app to Google for verification](https://support.google.com/cloud/answer/10311615).
You can change this domain in the settings for your Google app. Go to Google App Platform, click on Branding, and then set your application home page, privacy policy, and terms of use. You can also add your logo.
Next, go to Audience and click publish. Google will need to verify the app, which usually takes around 24 hours.
@@ -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
View File
@@ -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.
@@ -92,7 +92,7 @@ This approach has several benefits:
1. **On-demand:** analytical data is immediately available within your application with no additional infrastructure.
1. **Always in sync:** since the data is queried directly from the remote server, it's always up-to-date.
1. **Integrated:** large datasets are available within your application, and can be joined with your operational/transactional data.
1. **Save on bandwidth:** only extract/load what you need.
1. **Save on egress:** only extract/load what you need.
### Batch ETL with Wrappers
Loaded 100 of 636 files, more files were not shown because too many files have changed in this diff. Show more