mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 05:39:11 +03:00
Merge branch 'master' into chore/search-snippet-context
This commit is contained in:
commit
d27e9fd570
412 files changed
+27188
-11758
No files matched your search
@@ -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
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# [LOCAL ENV] - NEVER USE PROD VALUES LOCALLY
|
||||
# as editing PayloadCMS typescript config INSTANTLY CHANGES DB SCHEMA in dev mode
|
||||
# The values set here are the default values provided when running `supabase start`.
|
||||
# Do not add secrets here, this file is git-tracked. Use .env.local instead.
|
||||
DATABASE_URI=postgresql://postgres:postgres@127.0.0.1:34322/postgres
|
||||
PAYLOAD_SECRET=secret
|
||||
|
||||
S3_BUCKET=cms
|
||||
S3_ACCESS_KEY_ID=625729a08b95bf1b7ff351a663f3a23c
|
||||
S3_SECRET_ACCESS_KEY=850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907
|
||||
S3_REGION=local
|
||||
S3_ENDPOINT=http://127.0.0.1:34321/storage/v1/s3
|
||||
|
||||
BLOG_APP_URL=http://localhost:3000
|
||||
NEXT_PUBLIC_SERVER_URL=http://localhost:3000/blog
|
||||
CRON_SECRET=secret
|
||||
PREVIEW_SECRET=secret
|
||||
@@ -1,13 +0,0 @@
|
||||
DATABASE_URI=postgres://postgres:<password>@127.0.0.1:5432/your-database-name
|
||||
PAYLOAD_SECRET=YOUR_SECRET_HERE
|
||||
|
||||
S3_BUCKET=
|
||||
S3_ACCESS_KEY_ID=
|
||||
S3_SECRET_ACCESS_KEY=
|
||||
S3_REGION=
|
||||
S3_ENDPOINT=
|
||||
|
||||
NEXT_PUBLIC_SERVER_URL=http://localhost:3000/blog
|
||||
CRON_SECRET=YOUR_CRON_SECRET_HERE
|
||||
PREVIEW_SECRET=YOUR_SECRET_HERE
|
||||
BLOG_APP_URL=http://localhost:3000
|
||||
File renamed without changes.
+2
-3
@@ -29,7 +29,8 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
!.env
|
||||
.env.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
@@ -38,6 +39,4 @@ yarn-error.log*
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
.env
|
||||
|
||||
/media
|
||||
@@ -0,0 +1,48 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
**/node_modules
|
||||
|
||||
# Build artifacts
|
||||
.next/cache
|
||||
.turbo
|
||||
.eslintcache
|
||||
|
||||
# Development files
|
||||
*.log
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Test files
|
||||
**/*.test.*
|
||||
**/*.spec.*
|
||||
__tests__
|
||||
**/__tests__
|
||||
coverage
|
||||
|
||||
# Documentation
|
||||
README.md
|
||||
*.md
|
||||
docs
|
||||
|
||||
# IDE files
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Other apps (since we're only deploying CMS)
|
||||
../studio
|
||||
../www
|
||||
../docs
|
||||
../design-system
|
||||
../ui-library
|
||||
../../examples
|
||||
../../tests
|
||||
../../supabase
|
||||
../../packages/ui-patterns
|
||||
../../packages/ui
|
||||
../../packages/shared-data
|
||||
@@ -15,7 +15,7 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
# Uncomment the following to use postgres
|
||||
# Uncomment the following to use postgres
|
||||
postgres:
|
||||
restart: always
|
||||
image: postgres:latest
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
import { withPayload } from '@payloadcms/next/withPayload'
|
||||
|
||||
import redirects from './redirects.js'
|
||||
const redirects = async () => {
|
||||
const internetExplorerRedirect = {
|
||||
destination: '/ie-incompatible.html',
|
||||
has: [
|
||||
{
|
||||
type: 'header',
|
||||
key: 'user-agent',
|
||||
value: '(.*Trident.*)', // all ie browsers
|
||||
},
|
||||
],
|
||||
permanent: false,
|
||||
source: '/:path((?!ie-incompatible.html$).*)', // all pages except the incompatibility page
|
||||
}
|
||||
|
||||
const redirects = [internetExplorerRedirect]
|
||||
|
||||
return redirects
|
||||
}
|
||||
|
||||
const NEXT_PUBLIC_SERVER_URL = process.env.VERCEL_PROJECT_PRODUCTION_URL
|
||||
? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
|
||||
|
||||
+32
-16
@@ -5,43 +5,59 @@
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_OPTIONS=--no-deprecation next build --turbopack",
|
||||
"ci": "pnpm migrate && pnpm build",
|
||||
"clean": "rimraf node_modules .next",
|
||||
"dev": "cross-env NODE_OPTIONS=--no-deprecation next dev --turbopack --port 3030",
|
||||
"dev:prod": "cross-env NODE_OPTIONS=--no-deprecation rm -rf .next && pnpm build && pnpm start",
|
||||
"devsafe": "rm -rf .next && cross-env NODE_OPTIONS=--no-deprecation next dev",
|
||||
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
|
||||
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
|
||||
"lint": "cross-env NODE_OPTIONS=--no-deprecation next lint",
|
||||
"migrate": "cross-env NODE_OPTIONS=--no-deprecation tsx scripts/migrate.ts",
|
||||
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
|
||||
"start": "cross-env NODE_OPTIONS=--no-deprecation next start",
|
||||
"ci": "cross-env NODE_OPTIONS=--no-deprecation payload migrate && pnpm build",
|
||||
"clean": "rimraf node_modules .next",
|
||||
"start": "cross-env NODE_OPTIONS=--no-deprecation next start --port 3030",
|
||||
"vercel-build": "pnpm migrate && pnpm build",
|
||||
"typecheck_IGNORED": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/db-postgres": "3.33.0",
|
||||
"@payloadcms/live-preview-react": "^3.33.0",
|
||||
"@payloadcms/next": "3.33.0",
|
||||
"@payloadcms/payload-cloud": "3.33.0",
|
||||
"@payloadcms/plugin-form-builder": "3.33.0",
|
||||
"@payloadcms/plugin-nested-docs": "3.33.0",
|
||||
"@payloadcms/plugin-seo": "3.33.0",
|
||||
"@payloadcms/richtext-lexical": "3.33.0",
|
||||
"@payloadcms/storage-s3": "3.33.0",
|
||||
"@payloadcms/ui": "3.33.0",
|
||||
"@payloadcms/admin-bar": "^3.50.0",
|
||||
"@payloadcms/db-postgres": "^3.50.0",
|
||||
"@payloadcms/live-preview-react": "^3.50.0",
|
||||
"@payloadcms/next": "3.50.0",
|
||||
"@payloadcms/payload-cloud": "3.50.0",
|
||||
"@payloadcms/plugin-form-builder": "3.50.0",
|
||||
"@payloadcms/plugin-nested-docs": "3.50.0",
|
||||
"@payloadcms/plugin-seo": "3.50.0",
|
||||
"@payloadcms/richtext-lexical": "3.50.0",
|
||||
"@payloadcms/storage-s3": "3.50.0",
|
||||
"@payloadcms/ui": "3.50.0",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^1.2.1",
|
||||
"common": "workspace:*",
|
||||
"config": "workspace:*",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint-config-supabase": "workspace:*",
|
||||
"graphql": "^16.8.1",
|
||||
"graphql": "^16.11.0",
|
||||
"image-size": "2.0.2",
|
||||
"lucide-react": "^0.511.0",
|
||||
"next": "catalog:",
|
||||
"payload": "3.33.0",
|
||||
"payload": "3.50.0",
|
||||
"pg": "^8.16.3",
|
||||
"prism-react-renderer": "^2.3.1",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"sharp": "0.32.6"
|
||||
"sharp": "0.32.6",
|
||||
"tailwind-merge": "^1.13.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "5.7.3"
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
const redirects = async () => {
|
||||
const internetExplorerRedirect = {
|
||||
destination: '/ie-incompatible.html',
|
||||
has: [
|
||||
{
|
||||
type: 'header',
|
||||
key: 'user-agent',
|
||||
value: '(.*Trident.*)', // all ie browsers
|
||||
},
|
||||
],
|
||||
permanent: false,
|
||||
source: '/:path((?!ie-incompatible.html$).*)', // all pages except the incompatibility page
|
||||
}
|
||||
|
||||
const redirects = [internetExplorerRedirect]
|
||||
|
||||
return redirects
|
||||
}
|
||||
|
||||
export default redirects
|
||||
@@ -0,0 +1,28 @@
|
||||
import payload from 'payload'
|
||||
// Use a minimal config for migrations to avoid importing the full app graph during build
|
||||
import payloadConfig from '../src/payload.migrate.config.ts'
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
await payload.init({ config: payloadConfig })
|
||||
|
||||
// Ensure non-interactive: remove any dev-mode migration sentinel rows
|
||||
try {
|
||||
await payload.delete({
|
||||
collection: 'payload-migrations',
|
||||
where: { batch: { equals: -1 } },
|
||||
})
|
||||
} catch {}
|
||||
|
||||
await payload.db.migrate()
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('✅ Payload migrations complete')
|
||||
process.exit(0)
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('❌ Payload migrations failed:', err)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AccessArgs, FieldAccess } from 'payload'
|
||||
import type { User } from '@/payload-types'
|
||||
import type { User } from '../payload-types'
|
||||
|
||||
type isAdmin = (args: AccessArgs<User>) => boolean
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AccessArgs } from 'payload'
|
||||
|
||||
import type { User } from '@/payload-types'
|
||||
import type { User } from '../payload-types'
|
||||
|
||||
type isAuthenticated = (args: AccessArgs<User>) => boolean
|
||||
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import type { Post, ArchiveBlock as ArchiveBlockProps } from '@/payload-types'
|
||||
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
import React from 'react'
|
||||
import RichText from '@/components/RichText'
|
||||
|
||||
import { CollectionArchive } from '@/components/CollectionArchive'
|
||||
|
||||
export const ArchiveBlock: React.FC<
|
||||
ArchiveBlockProps & {
|
||||
id?: string
|
||||
}
|
||||
> = async (props) => {
|
||||
const { id, categories, introContent, limit: limitFromProps, populateBy, selectedDocs } = props
|
||||
|
||||
const limit = limitFromProps || 3
|
||||
|
||||
let posts: Post[] = []
|
||||
|
||||
if (populateBy === 'collection') {
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
const flattenedCategories = categories?.map((category) => {
|
||||
if (typeof category === 'object') return category.id
|
||||
else return category
|
||||
})
|
||||
|
||||
const fetchedPosts = await payload.find({
|
||||
collection: 'posts',
|
||||
depth: 1,
|
||||
limit,
|
||||
...(flattenedCategories && flattenedCategories.length > 0
|
||||
? {
|
||||
where: {
|
||||
categories: {
|
||||
in: flattenedCategories,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
|
||||
posts = fetchedPosts.docs
|
||||
} else {
|
||||
if (selectedDocs?.length) {
|
||||
const filteredSelectedPosts = selectedDocs.map((post) => {
|
||||
if (typeof post.value === 'object') return post.value
|
||||
}) as Post[]
|
||||
|
||||
posts = filteredSelectedPosts
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="my-16" id={`block-${id}`}>
|
||||
{introContent && (
|
||||
<div className="container mb-16">
|
||||
<RichText className="ms-0 max-w-[48rem]" data={introContent} enableGutter={false} />
|
||||
</div>
|
||||
)}
|
||||
<CollectionArchive posts={posts} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import type { Block } from 'payload'
|
||||
|
||||
import {
|
||||
FixedToolbarFeature,
|
||||
HeadingFeature,
|
||||
InlineToolbarFeature,
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
export const Archive: Block = {
|
||||
slug: 'archive',
|
||||
interfaceName: 'ArchiveBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'introContent',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ rootFeatures }) => {
|
||||
return [
|
||||
...rootFeatures,
|
||||
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
|
||||
FixedToolbarFeature(),
|
||||
InlineToolbarFeature(),
|
||||
]
|
||||
},
|
||||
}),
|
||||
label: 'Intro Content',
|
||||
},
|
||||
{
|
||||
name: 'populateBy',
|
||||
type: 'select',
|
||||
defaultValue: 'collection',
|
||||
options: [
|
||||
{
|
||||
label: 'Collection',
|
||||
value: 'collection',
|
||||
},
|
||||
{
|
||||
label: 'Individual Selection',
|
||||
value: 'selection',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'relationTo',
|
||||
type: 'select',
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData.populateBy === 'collection',
|
||||
},
|
||||
defaultValue: 'posts',
|
||||
label: 'Collections To Show',
|
||||
options: [
|
||||
{
|
||||
label: 'Posts',
|
||||
value: 'posts',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'categories',
|
||||
type: 'relationship',
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData.populateBy === 'collection',
|
||||
},
|
||||
hasMany: true,
|
||||
label: 'Categories To Show',
|
||||
relationTo: 'categories',
|
||||
},
|
||||
{
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData.populateBy === 'collection',
|
||||
step: 1,
|
||||
},
|
||||
defaultValue: 10,
|
||||
label: 'Limit',
|
||||
},
|
||||
{
|
||||
name: 'selectedDocs',
|
||||
type: 'relationship',
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData.populateBy === 'selection',
|
||||
},
|
||||
hasMany: true,
|
||||
label: 'Selection',
|
||||
relationTo: ['posts'],
|
||||
},
|
||||
],
|
||||
labels: {
|
||||
plural: 'Archives',
|
||||
singular: 'Archive',
|
||||
},
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import type { CallToActionBlock as CTABlockProps } from '@/payload-types'
|
||||
|
||||
import RichText from '@/components/RichText'
|
||||
import { CMSLink } from '@/components/Link'
|
||||
|
||||
export const CallToActionBlock: React.FC<CTABlockProps> = ({ links, richText }) => {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="bg-card rounded border-border border p-4 flex flex-col gap-8 md:flex-row md:justify-between md:items-center">
|
||||
<div className="max-w-[48rem] flex items-center">
|
||||
{richText && <RichText className="mb-0" data={richText} enableGutter={false} />}
|
||||
</div>
|
||||
<div className="flex flex-col gap-8">
|
||||
{(links || []).map(({ link }, i) => {
|
||||
return <CMSLink key={i} size="lg" {...link} />
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import type { Block } from 'payload'
|
||||
|
||||
import {
|
||||
FixedToolbarFeature,
|
||||
HeadingFeature,
|
||||
InlineToolbarFeature,
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { linkGroup } from '../../fields/linkGroup'
|
||||
|
||||
export const CallToAction: Block = {
|
||||
slug: 'cta',
|
||||
interfaceName: 'CallToActionBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'richText',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ rootFeatures }) => {
|
||||
return [
|
||||
...rootFeatures,
|
||||
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
|
||||
FixedToolbarFeature(),
|
||||
InlineToolbarFeature(),
|
||||
]
|
||||
},
|
||||
}),
|
||||
label: false,
|
||||
},
|
||||
linkGroup({
|
||||
appearances: ['default', 'outline'],
|
||||
overrides: {
|
||||
maxRows: 2,
|
||||
},
|
||||
}),
|
||||
],
|
||||
labels: {
|
||||
plural: 'Calls to Action',
|
||||
singular: 'Call to Action',
|
||||
},
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export function CopyButton({ code }: { code: string }) {
|
||||
<div className="flex justify-end align-middle">
|
||||
<Button
|
||||
className="flex gap-1"
|
||||
variant={'secondary'}
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(code)
|
||||
updateCopyStatus()
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { cn } from '@/utilities/ui'
|
||||
import React from 'react'
|
||||
import RichText from '@/components/RichText'
|
||||
|
||||
import type { ContentBlock as ContentBlockProps } from '@/payload-types'
|
||||
|
||||
import { CMSLink } from '../../components/Link'
|
||||
|
||||
export const ContentBlock: React.FC<ContentBlockProps> = (props) => {
|
||||
const { columns } = props
|
||||
|
||||
const colsSpanClasses = {
|
||||
full: '12',
|
||||
half: '6',
|
||||
oneThird: '4',
|
||||
twoThirds: '8',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container my-16">
|
||||
<div className="grid grid-cols-4 lg:grid-cols-12 gap-y-8 gap-x-16">
|
||||
{columns &&
|
||||
columns.length > 0 &&
|
||||
columns.map((col, index) => {
|
||||
const { enableLink, link, richText, size } = col
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(`col-span-4 lg:col-span-${colsSpanClasses[size!]}`, {
|
||||
'md:col-span-2': size !== 'full',
|
||||
})}
|
||||
key={index}
|
||||
>
|
||||
{richText && <RichText data={richText} enableGutter={false} />}
|
||||
|
||||
{enableLink && <CMSLink {...link} />}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import type { Block, Field } from 'payload'
|
||||
|
||||
import {
|
||||
FixedToolbarFeature,
|
||||
HeadingFeature,
|
||||
InlineToolbarFeature,
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { link } from '@/fields/link'
|
||||
|
||||
const columnFields: Field[] = [
|
||||
{
|
||||
name: 'size',
|
||||
type: 'select',
|
||||
defaultValue: 'oneThird',
|
||||
options: [
|
||||
{
|
||||
label: 'One Third',
|
||||
value: 'oneThird',
|
||||
},
|
||||
{
|
||||
label: 'Half',
|
||||
value: 'half',
|
||||
},
|
||||
{
|
||||
label: 'Two Thirds',
|
||||
value: 'twoThirds',
|
||||
},
|
||||
{
|
||||
label: 'Full',
|
||||
value: 'full',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'richText',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ rootFeatures }) => {
|
||||
return [
|
||||
...rootFeatures,
|
||||
HeadingFeature({ enabledHeadingSizes: ['h2', 'h3', 'h4'] }),
|
||||
FixedToolbarFeature(),
|
||||
InlineToolbarFeature(),
|
||||
]
|
||||
},
|
||||
}),
|
||||
label: false,
|
||||
},
|
||||
{
|
||||
name: 'enableLink',
|
||||
type: 'checkbox',
|
||||
},
|
||||
link({
|
||||
overrides: {
|
||||
admin: {
|
||||
condition: (_data, siblingData) => {
|
||||
return Boolean(siblingData?.enableLink)
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
||||
export const Content: Block = {
|
||||
slug: 'content',
|
||||
interfaceName: 'ContentBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'columns',
|
||||
type: 'array',
|
||||
admin: {
|
||||
initCollapsed: true,
|
||||
},
|
||||
fields: columnFields,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import type { CheckboxField } from '@payloadcms/plugin-form-builder/types'
|
||||
import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form'
|
||||
|
||||
import { useFormContext } from 'react-hook-form'
|
||||
|
||||
import { Checkbox as CheckboxUi } from '@/components/ui/checkbox'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import React from 'react'
|
||||
|
||||
import { Error } from '../Error'
|
||||
import { Width } from '../Width'
|
||||
|
||||
export const Checkbox: React.FC<
|
||||
CheckboxField & {
|
||||
errors: Partial<FieldErrorsImpl>
|
||||
register: UseFormRegister<FieldValues>
|
||||
}
|
||||
> = ({ name, defaultValue, errors, label, register, required, width }) => {
|
||||
const props = register(name, { required: required })
|
||||
const { setValue } = useFormContext()
|
||||
|
||||
return (
|
||||
<Width width={width}>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckboxUi
|
||||
defaultChecked={defaultValue}
|
||||
id={name}
|
||||
{...props}
|
||||
onCheckedChange={(checked) => {
|
||||
setValue(props.name, checked)
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor={name}>
|
||||
{required && (
|
||||
<span className="required">
|
||||
* <span className="sr-only">(required)</span>
|
||||
</span>
|
||||
)}
|
||||
{label}
|
||||
</Label>
|
||||
</div>
|
||||
{errors[name] && <Error name={name} />}
|
||||
</Width>
|
||||
)
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
'use client'
|
||||
import type { FormFieldBlock, Form as FormType } from '@payloadcms/plugin-form-builder/types'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useForm, FormProvider } from 'react-hook-form'
|
||||
import RichText from '@/components/RichText'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
import { fields } from './fields'
|
||||
import { getClientSideURL } from '@/utilities/getURL'
|
||||
|
||||
export type FormBlockType = {
|
||||
blockName?: string
|
||||
blockType?: 'formBlock'
|
||||
enableIntro: boolean
|
||||
form: FormType
|
||||
introContent?: SerializedEditorState
|
||||
}
|
||||
|
||||
export const FormBlock: React.FC<
|
||||
{
|
||||
id?: string
|
||||
} & FormBlockType
|
||||
> = (props) => {
|
||||
const {
|
||||
enableIntro,
|
||||
form: formFromProps,
|
||||
form: { id: formID, confirmationMessage, confirmationType, redirect, submitButtonLabel } = {},
|
||||
introContent,
|
||||
} = props
|
||||
|
||||
const formMethods = useForm({
|
||||
defaultValues: formFromProps.fields,
|
||||
})
|
||||
const {
|
||||
control,
|
||||
formState: { errors },
|
||||
handleSubmit,
|
||||
register,
|
||||
} = formMethods
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [hasSubmitted, setHasSubmitted] = useState<boolean>()
|
||||
const [error, setError] = useState<{ message: string; status?: string } | undefined>()
|
||||
const router = useRouter()
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(data: FormFieldBlock[]) => {
|
||||
let loadingTimerID: ReturnType<typeof setTimeout>
|
||||
const submitForm = async () => {
|
||||
setError(undefined)
|
||||
|
||||
const dataToSend = Object.entries(data).map(([name, value]) => ({
|
||||
field: name,
|
||||
value,
|
||||
}))
|
||||
|
||||
// delay loading indicator by 1s
|
||||
loadingTimerID = setTimeout(() => {
|
||||
setIsLoading(true)
|
||||
}, 1000)
|
||||
|
||||
try {
|
||||
const req = await fetch(`${getClientSideURL()}/api/form-submissions`, {
|
||||
body: JSON.stringify({
|
||||
form: formID,
|
||||
submissionData: dataToSend,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
const res = await req.json()
|
||||
|
||||
clearTimeout(loadingTimerID)
|
||||
|
||||
if (req.status >= 400) {
|
||||
setIsLoading(false)
|
||||
|
||||
setError({
|
||||
message: res.errors?.[0]?.message || 'Internal Server Error',
|
||||
status: res.status,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
setHasSubmitted(true)
|
||||
|
||||
if (confirmationType === 'redirect' && redirect) {
|
||||
const { url } = redirect
|
||||
|
||||
const redirectUrl = url
|
||||
|
||||
if (redirectUrl) router.push(redirectUrl)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
setIsLoading(false)
|
||||
setError({
|
||||
message: 'Something went wrong.',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
void submitForm()
|
||||
},
|
||||
[router, formID, redirect, confirmationType]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="container lg:max-w-[48rem]">
|
||||
{enableIntro && introContent && !hasSubmitted && (
|
||||
<RichText className="mb-8 lg:mb-12" data={introContent} enableGutter={false} />
|
||||
)}
|
||||
<div className="p-4 lg:p-6 border border-border rounded-[0.8rem]">
|
||||
<FormProvider {...formMethods}>
|
||||
{!isLoading && hasSubmitted && confirmationType === 'message' && (
|
||||
<RichText data={confirmationMessage} />
|
||||
)}
|
||||
{isLoading && !hasSubmitted && <p>Loading, please wait...</p>}
|
||||
{error && <div>{`${error.status || '500'}: ${error.message || ''}`}</div>}
|
||||
{!hasSubmitted && (
|
||||
<form id={formID} onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="mb-4 last:mb-0">
|
||||
{formFromProps &&
|
||||
formFromProps.fields &&
|
||||
formFromProps.fields?.map((field, index) => {
|
||||
const Field: React.FC<any> = fields?.[field.blockType as keyof typeof fields]
|
||||
if (Field) {
|
||||
return (
|
||||
<div className="mb-6 last:mb-0" key={index}>
|
||||
<Field
|
||||
form={formFromProps}
|
||||
{...field}
|
||||
{...formMethods}
|
||||
control={control}
|
||||
errors={errors}
|
||||
register={register}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Button form={formID} type="submit" variant="default">
|
||||
{submitButtonLabel}
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import type { CountryField } from '@payloadcms/plugin-form-builder/types'
|
||||
import type { Control, FieldErrorsImpl } from 'react-hook-form'
|
||||
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import React from 'react'
|
||||
import { Controller } from 'react-hook-form'
|
||||
|
||||
import { Error } from '../Error'
|
||||
import { Width } from '../Width'
|
||||
import { countryOptions } from './options'
|
||||
|
||||
export const Country: React.FC<
|
||||
CountryField & {
|
||||
control: Control
|
||||
errors: Partial<FieldErrorsImpl>
|
||||
}
|
||||
> = ({ name, control, errors, label, required, width }) => {
|
||||
return (
|
||||
<Width width={width}>
|
||||
<Label className="" htmlFor={name}>
|
||||
{label}
|
||||
|
||||
{required && (
|
||||
<span className="required">
|
||||
* <span className="sr-only">(required)</span>
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name={name}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const controlledValue = countryOptions.find((t) => t.value === value)
|
||||
|
||||
return (
|
||||
<Select onValueChange={(val) => onChange(val)} value={controlledValue?.value}>
|
||||
<SelectTrigger className="w-full" id={name}>
|
||||
<SelectValue placeholder={label} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{countryOptions.map(({ label, value }) => {
|
||||
return (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
)
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
}}
|
||||
rules={{ required }}
|
||||
/>
|
||||
{errors[name] && <Error name={name} />}
|
||||
</Width>
|
||||
)
|
||||
}
|
||||
@@ -1,982 +0,0 @@
|
||||
export const countryOptions = [
|
||||
{
|
||||
label: 'Afghanistan',
|
||||
value: 'AF',
|
||||
},
|
||||
{
|
||||
label: 'Åland Islands',
|
||||
value: 'AX',
|
||||
},
|
||||
{
|
||||
label: 'Albania',
|
||||
value: 'AL',
|
||||
},
|
||||
{
|
||||
label: 'Algeria',
|
||||
value: 'DZ',
|
||||
},
|
||||
{
|
||||
label: 'American Samoa',
|
||||
value: 'AS',
|
||||
},
|
||||
{
|
||||
label: 'Andorra',
|
||||
value: 'AD',
|
||||
},
|
||||
{
|
||||
label: 'Angola',
|
||||
value: 'AO',
|
||||
},
|
||||
{
|
||||
label: 'Anguilla',
|
||||
value: 'AI',
|
||||
},
|
||||
{
|
||||
label: 'Antarctica',
|
||||
value: 'AQ',
|
||||
},
|
||||
{
|
||||
label: 'Antigua and Barbuda',
|
||||
value: 'AG',
|
||||
},
|
||||
{
|
||||
label: 'Argentina',
|
||||
value: 'AR',
|
||||
},
|
||||
{
|
||||
label: 'Armenia',
|
||||
value: 'AM',
|
||||
},
|
||||
{
|
||||
label: 'Aruba',
|
||||
value: 'AW',
|
||||
},
|
||||
{
|
||||
label: 'Australia',
|
||||
value: 'AU',
|
||||
},
|
||||
{
|
||||
label: 'Austria',
|
||||
value: 'AT',
|
||||
},
|
||||
{
|
||||
label: 'Azerbaijan',
|
||||
value: 'AZ',
|
||||
},
|
||||
{
|
||||
label: 'Bahamas',
|
||||
value: 'BS',
|
||||
},
|
||||
{
|
||||
label: 'Bahrain',
|
||||
value: 'BH',
|
||||
},
|
||||
{
|
||||
label: 'Bangladesh',
|
||||
value: 'BD',
|
||||
},
|
||||
{
|
||||
label: 'Barbados',
|
||||
value: 'BB',
|
||||
},
|
||||
{
|
||||
label: 'Belarus',
|
||||
value: 'BY',
|
||||
},
|
||||
{
|
||||
label: 'Belgium',
|
||||
value: 'BE',
|
||||
},
|
||||
{
|
||||
label: 'Belize',
|
||||
value: 'BZ',
|
||||
},
|
||||
{
|
||||
label: 'Benin',
|
||||
value: 'BJ',
|
||||
},
|
||||
{
|
||||
label: 'Bermuda',
|
||||
value: 'BM',
|
||||
},
|
||||
{
|
||||
label: 'Bhutan',
|
||||
value: 'BT',
|
||||
},
|
||||
{
|
||||
label: 'Bolivia',
|
||||
value: 'BO',
|
||||
},
|
||||
{
|
||||
label: 'Bosnia and Herzegovina',
|
||||
value: 'BA',
|
||||
},
|
||||
{
|
||||
label: 'Botswana',
|
||||
value: 'BW',
|
||||
},
|
||||
{
|
||||
label: 'Bouvet Island',
|
||||
value: 'BV',
|
||||
},
|
||||
{
|
||||
label: 'Brazil',
|
||||
value: 'BR',
|
||||
},
|
||||
{
|
||||
label: 'British Indian Ocean Territory',
|
||||
value: 'IO',
|
||||
},
|
||||
{
|
||||
label: 'Brunei Darussalam',
|
||||
value: 'BN',
|
||||
},
|
||||
{
|
||||
label: 'Bulgaria',
|
||||
value: 'BG',
|
||||
},
|
||||
{
|
||||
label: 'Burkina Faso',
|
||||
value: 'BF',
|
||||
},
|
||||
{
|
||||
label: 'Burundi',
|
||||
value: 'BI',
|
||||
},
|
||||
{
|
||||
label: 'Cambodia',
|
||||
value: 'KH',
|
||||
},
|
||||
{
|
||||
label: 'Cameroon',
|
||||
value: 'CM',
|
||||
},
|
||||
{
|
||||
label: 'Canada',
|
||||
value: 'CA',
|
||||
},
|
||||
{
|
||||
label: 'Cape Verde',
|
||||
value: 'CV',
|
||||
},
|
||||
{
|
||||
label: 'Cayman Islands',
|
||||
value: 'KY',
|
||||
},
|
||||
{
|
||||
label: 'Central African Republic',
|
||||
value: 'CF',
|
||||
},
|
||||
{
|
||||
label: 'Chad',
|
||||
value: 'TD',
|
||||
},
|
||||
{
|
||||
label: 'Chile',
|
||||
value: 'CL',
|
||||
},
|
||||
{
|
||||
label: 'China',
|
||||
value: 'CN',
|
||||
},
|
||||
{
|
||||
label: 'Christmas Island',
|
||||
value: 'CX',
|
||||
},
|
||||
{
|
||||
label: 'Cocos (Keeling) Islands',
|
||||
value: 'CC',
|
||||
},
|
||||
{
|
||||
label: 'Colombia',
|
||||
value: 'CO',
|
||||
},
|
||||
{
|
||||
label: 'Comoros',
|
||||
value: 'KM',
|
||||
},
|
||||
{
|
||||
label: 'Congo',
|
||||
value: 'CG',
|
||||
},
|
||||
{
|
||||
label: 'Congo, The Democratic Republic of the',
|
||||
value: 'CD',
|
||||
},
|
||||
{
|
||||
label: 'Cook Islands',
|
||||
value: 'CK',
|
||||
},
|
||||
{
|
||||
label: 'Costa Rica',
|
||||
value: 'CR',
|
||||
},
|
||||
{
|
||||
label: "Cote D'Ivoire",
|
||||
value: 'CI',
|
||||
},
|
||||
{
|
||||
label: 'Croatia',
|
||||
value: 'HR',
|
||||
},
|
||||
{
|
||||
label: 'Cuba',
|
||||
value: 'CU',
|
||||
},
|
||||
{
|
||||
label: 'Cyprus',
|
||||
value: 'CY',
|
||||
},
|
||||
{
|
||||
label: 'Czech Republic',
|
||||
value: 'CZ',
|
||||
},
|
||||
{
|
||||
label: 'Denmark',
|
||||
value: 'DK',
|
||||
},
|
||||
{
|
||||
label: 'Djibouti',
|
||||
value: 'DJ',
|
||||
},
|
||||
{
|
||||
label: 'Dominica',
|
||||
value: 'DM',
|
||||
},
|
||||
{
|
||||
label: 'Dominican Republic',
|
||||
value: 'DO',
|
||||
},
|
||||
{
|
||||
label: 'Ecuador',
|
||||
value: 'EC',
|
||||
},
|
||||
{
|
||||
label: 'Egypt',
|
||||
value: 'EG',
|
||||
},
|
||||
{
|
||||
label: 'El Salvador',
|
||||
value: 'SV',
|
||||
},
|
||||
{
|
||||
label: 'Equatorial Guinea',
|
||||
value: 'GQ',
|
||||
},
|
||||
{
|
||||
label: 'Eritrea',
|
||||
value: 'ER',
|
||||
},
|
||||
{
|
||||
label: 'Estonia',
|
||||
value: 'EE',
|
||||
},
|
||||
{
|
||||
label: 'Ethiopia',
|
||||
value: 'ET',
|
||||
},
|
||||
{
|
||||
label: 'Falkland Islands (Malvinas)',
|
||||
value: 'FK',
|
||||
},
|
||||
{
|
||||
label: 'Faroe Islands',
|
||||
value: 'FO',
|
||||
},
|
||||
{
|
||||
label: 'Fiji',
|
||||
value: 'FJ',
|
||||
},
|
||||
{
|
||||
label: 'Finland',
|
||||
value: 'FI',
|
||||
},
|
||||
{
|
||||
label: 'France',
|
||||
value: 'FR',
|
||||
},
|
||||
{
|
||||
label: 'French Guiana',
|
||||
value: 'GF',
|
||||
},
|
||||
{
|
||||
label: 'French Polynesia',
|
||||
value: 'PF',
|
||||
},
|
||||
{
|
||||
label: 'French Southern Territories',
|
||||
value: 'TF',
|
||||
},
|
||||
{
|
||||
label: 'Gabon',
|
||||
value: 'GA',
|
||||
},
|
||||
{
|
||||
label: 'Gambia',
|
||||
value: 'GM',
|
||||
},
|
||||
{
|
||||
label: 'Georgia',
|
||||
value: 'GE',
|
||||
},
|
||||
{
|
||||
label: 'Germany',
|
||||
value: 'DE',
|
||||
},
|
||||
{
|
||||
label: 'Ghana',
|
||||
value: 'GH',
|
||||
},
|
||||
{
|
||||
label: 'Gibraltar',
|
||||
value: 'GI',
|
||||
},
|
||||
{
|
||||
label: 'Greece',
|
||||
value: 'GR',
|
||||
},
|
||||
{
|
||||
label: 'Greenland',
|
||||
value: 'GL',
|
||||
},
|
||||
{
|
||||
label: 'Grenada',
|
||||
value: 'GD',
|
||||
},
|
||||
{
|
||||
label: 'Guadeloupe',
|
||||
value: 'GP',
|
||||
},
|
||||
{
|
||||
label: 'Guam',
|
||||
value: 'GU',
|
||||
},
|
||||
{
|
||||
label: 'Guatemala',
|
||||
value: 'GT',
|
||||
},
|
||||
{
|
||||
label: 'Guernsey',
|
||||
value: 'GG',
|
||||
},
|
||||
{
|
||||
label: 'Guinea',
|
||||
value: 'GN',
|
||||
},
|
||||
{
|
||||
label: 'Guinea-Bissau',
|
||||
value: 'GW',
|
||||
},
|
||||
{
|
||||
label: 'Guyana',
|
||||
value: 'GY',
|
||||
},
|
||||
{
|
||||
label: 'Haiti',
|
||||
value: 'HT',
|
||||
},
|
||||
{
|
||||
label: 'Heard Island and Mcdonald Islands',
|
||||
value: 'HM',
|
||||
},
|
||||
{
|
||||
label: 'Holy See (Vatican City State)',
|
||||
value: 'VA',
|
||||
},
|
||||
{
|
||||
label: 'Honduras',
|
||||
value: 'HN',
|
||||
},
|
||||
{
|
||||
label: 'Hong Kong',
|
||||
value: 'HK',
|
||||
},
|
||||
{
|
||||
label: 'Hungary',
|
||||
value: 'HU',
|
||||
},
|
||||
{
|
||||
label: 'Iceland',
|
||||
value: 'IS',
|
||||
},
|
||||
{
|
||||
label: 'India',
|
||||
value: 'IN',
|
||||
},
|
||||
{
|
||||
label: 'Indonesia',
|
||||
value: 'ID',
|
||||
},
|
||||
{
|
||||
label: 'Iran, Islamic Republic Of',
|
||||
value: 'IR',
|
||||
},
|
||||
{
|
||||
label: 'Iraq',
|
||||
value: 'IQ',
|
||||
},
|
||||
{
|
||||
label: 'Ireland',
|
||||
value: 'IE',
|
||||
},
|
||||
{
|
||||
label: 'Isle of Man',
|
||||
value: 'IM',
|
||||
},
|
||||
{
|
||||
label: 'Israel',
|
||||
value: 'IL',
|
||||
},
|
||||
{
|
||||
label: 'Italy',
|
||||
value: 'IT',
|
||||
},
|
||||
{
|
||||
label: 'Jamaica',
|
||||
value: 'JM',
|
||||
},
|
||||
{
|
||||
label: 'Japan',
|
||||
value: 'JP',
|
||||
},
|
||||
{
|
||||
label: 'Jersey',
|
||||
value: 'JE',
|
||||
},
|
||||
{
|
||||
label: 'Jordan',
|
||||
value: 'JO',
|
||||
},
|
||||
{
|
||||
label: 'Kazakhstan',
|
||||
value: 'KZ',
|
||||
},
|
||||
{
|
||||
label: 'Kenya',
|
||||
value: 'KE',
|
||||
},
|
||||
{
|
||||
label: 'Kiribati',
|
||||
value: 'KI',
|
||||
},
|
||||
{
|
||||
label: "Democratic People's Republic of Korea",
|
||||
value: 'KP',
|
||||
},
|
||||
{
|
||||
label: 'Korea, Republic of',
|
||||
value: 'KR',
|
||||
},
|
||||
{
|
||||
label: 'Kosovo',
|
||||
value: 'XK',
|
||||
},
|
||||
{
|
||||
label: 'Kuwait',
|
||||
value: 'KW',
|
||||
},
|
||||
{
|
||||
label: 'Kyrgyzstan',
|
||||
value: 'KG',
|
||||
},
|
||||
{
|
||||
label: "Lao People's Democratic Republic",
|
||||
value: 'LA',
|
||||
},
|
||||
{
|
||||
label: 'Latvia',
|
||||
value: 'LV',
|
||||
},
|
||||
{
|
||||
label: 'Lebanon',
|
||||
value: 'LB',
|
||||
},
|
||||
{
|
||||
label: 'Lesotho',
|
||||
value: 'LS',
|
||||
},
|
||||
{
|
||||
label: 'Liberia',
|
||||
value: 'LR',
|
||||
},
|
||||
{
|
||||
label: 'Libyan Arab Jamahiriya',
|
||||
value: 'LY',
|
||||
},
|
||||
{
|
||||
label: 'Liechtenstein',
|
||||
value: 'LI',
|
||||
},
|
||||
{
|
||||
label: 'Lithuania',
|
||||
value: 'LT',
|
||||
},
|
||||
{
|
||||
label: 'Luxembourg',
|
||||
value: 'LU',
|
||||
},
|
||||
{
|
||||
label: 'Macao',
|
||||
value: 'MO',
|
||||
},
|
||||
{
|
||||
label: 'Macedonia, The Former Yugoslav Republic of',
|
||||
value: 'MK',
|
||||
},
|
||||
{
|
||||
label: 'Madagascar',
|
||||
value: 'MG',
|
||||
},
|
||||
{
|
||||
label: 'Malawi',
|
||||
value: 'MW',
|
||||
},
|
||||
{
|
||||
label: 'Malaysia',
|
||||
value: 'MY',
|
||||
},
|
||||
{
|
||||
label: 'Maldives',
|
||||
value: 'MV',
|
||||
},
|
||||
{
|
||||
label: 'Mali',
|
||||
value: 'ML',
|
||||
},
|
||||
{
|
||||
label: 'Malta',
|
||||
value: 'MT',
|
||||
},
|
||||
{
|
||||
label: 'Marshall Islands',
|
||||
value: 'MH',
|
||||
},
|
||||
{
|
||||
label: 'Martinique',
|
||||
value: 'MQ',
|
||||
},
|
||||
{
|
||||
label: 'Mauritania',
|
||||
value: 'MR',
|
||||
},
|
||||
{
|
||||
label: 'Mauritius',
|
||||
value: 'MU',
|
||||
},
|
||||
{
|
||||
label: 'Mayotte',
|
||||
value: 'YT',
|
||||
},
|
||||
{
|
||||
label: 'Mexico',
|
||||
value: 'MX',
|
||||
},
|
||||
{
|
||||
label: 'Micronesia, Federated States of',
|
||||
value: 'FM',
|
||||
},
|
||||
{
|
||||
label: 'Moldova, Republic of',
|
||||
value: 'MD',
|
||||
},
|
||||
{
|
||||
label: 'Monaco',
|
||||
value: 'MC',
|
||||
},
|
||||
{
|
||||
label: 'Mongolia',
|
||||
value: 'MN',
|
||||
},
|
||||
{
|
||||
label: 'Montenegro',
|
||||
value: 'ME',
|
||||
},
|
||||
{
|
||||
label: 'Montserrat',
|
||||
value: 'MS',
|
||||
},
|
||||
{
|
||||
label: 'Morocco',
|
||||
value: 'MA',
|
||||
},
|
||||
{
|
||||
label: 'Mozambique',
|
||||
value: 'MZ',
|
||||
},
|
||||
{
|
||||
label: 'Myanmar',
|
||||
value: 'MM',
|
||||
},
|
||||
{
|
||||
label: 'Namibia',
|
||||
value: 'NA',
|
||||
},
|
||||
{
|
||||
label: 'Nauru',
|
||||
value: 'NR',
|
||||
},
|
||||
{
|
||||
label: 'Nepal',
|
||||
value: 'NP',
|
||||
},
|
||||
{
|
||||
label: 'Netherlands',
|
||||
value: 'NL',
|
||||
},
|
||||
{
|
||||
label: 'Netherlands Antilles',
|
||||
value: 'AN',
|
||||
},
|
||||
{
|
||||
label: 'New Caledonia',
|
||||
value: 'NC',
|
||||
},
|
||||
{
|
||||
label: 'New Zealand',
|
||||
value: 'NZ',
|
||||
},
|
||||
{
|
||||
label: 'Nicaragua',
|
||||
value: 'NI',
|
||||
},
|
||||
{
|
||||
label: 'Niger',
|
||||
value: 'NE',
|
||||
},
|
||||
{
|
||||
label: 'Nigeria',
|
||||
value: 'NG',
|
||||
},
|
||||
{
|
||||
label: 'Niue',
|
||||
value: 'NU',
|
||||
},
|
||||
{
|
||||
label: 'Norfolk Island',
|
||||
value: 'NF',
|
||||
},
|
||||
{
|
||||
label: 'Northern Mariana Islands',
|
||||
value: 'MP',
|
||||
},
|
||||
{
|
||||
label: 'Norway',
|
||||
value: 'NO',
|
||||
},
|
||||
{
|
||||
label: 'Oman',
|
||||
value: 'OM',
|
||||
},
|
||||
{
|
||||
label: 'Pakistan',
|
||||
value: 'PK',
|
||||
},
|
||||
{
|
||||
label: 'Palau',
|
||||
value: 'PW',
|
||||
},
|
||||
{
|
||||
label: 'Palestinian Territory, Occupied',
|
||||
value: 'PS',
|
||||
},
|
||||
{
|
||||
label: 'Panama',
|
||||
value: 'PA',
|
||||
},
|
||||
{
|
||||
label: 'Papua New Guinea',
|
||||
value: 'PG',
|
||||
},
|
||||
{
|
||||
label: 'Paraguay',
|
||||
value: 'PY',
|
||||
},
|
||||
{
|
||||
label: 'Peru',
|
||||
value: 'PE',
|
||||
},
|
||||
{
|
||||
label: 'Philippines',
|
||||
value: 'PH',
|
||||
},
|
||||
{
|
||||
label: 'Pitcairn',
|
||||
value: 'PN',
|
||||
},
|
||||
{
|
||||
label: 'Poland',
|
||||
value: 'PL',
|
||||
},
|
||||
{
|
||||
label: 'Portugal',
|
||||
value: 'PT',
|
||||
},
|
||||
{
|
||||
label: 'Puerto Rico',
|
||||
value: 'PR',
|
||||
},
|
||||
{
|
||||
label: 'Qatar',
|
||||
value: 'QA',
|
||||
},
|
||||
{
|
||||
label: 'Reunion',
|
||||
value: 'RE',
|
||||
},
|
||||
{
|
||||
label: 'Romania',
|
||||
value: 'RO',
|
||||
},
|
||||
{
|
||||
label: 'Russian Federation',
|
||||
value: 'RU',
|
||||
},
|
||||
{
|
||||
label: 'Rwanda',
|
||||
value: 'RW',
|
||||
},
|
||||
{
|
||||
label: 'Saint Helena',
|
||||
value: 'SH',
|
||||
},
|
||||
{
|
||||
label: 'Saint Kitts and Nevis',
|
||||
value: 'KN',
|
||||
},
|
||||
{
|
||||
label: 'Saint Lucia',
|
||||
value: 'LC',
|
||||
},
|
||||
{
|
||||
label: 'Saint Pierre and Miquelon',
|
||||
value: 'PM',
|
||||
},
|
||||
{
|
||||
label: 'Saint Vincent and the Grenadines',
|
||||
value: 'VC',
|
||||
},
|
||||
{
|
||||
label: 'Samoa',
|
||||
value: 'WS',
|
||||
},
|
||||
{
|
||||
label: 'San Marino',
|
||||
value: 'SM',
|
||||
},
|
||||
{
|
||||
label: 'Sao Tome and Principe',
|
||||
value: 'ST',
|
||||
},
|
||||
{
|
||||
label: 'Saudi Arabia',
|
||||
value: 'SA',
|
||||
},
|
||||
{
|
||||
label: 'Senegal',
|
||||
value: 'SN',
|
||||
},
|
||||
{
|
||||
label: 'Serbia',
|
||||
value: 'RS',
|
||||
},
|
||||
{
|
||||
label: 'Seychelles',
|
||||
value: 'SC',
|
||||
},
|
||||
{
|
||||
label: 'Sierra Leone',
|
||||
value: 'SL',
|
||||
},
|
||||
{
|
||||
label: 'Singapore',
|
||||
value: 'SG',
|
||||
},
|
||||
{
|
||||
label: 'Slovakia',
|
||||
value: 'SK',
|
||||
},
|
||||
{
|
||||
label: 'Slovenia',
|
||||
value: 'SI',
|
||||
},
|
||||
{
|
||||
label: 'Solomon Islands',
|
||||
value: 'SB',
|
||||
},
|
||||
{
|
||||
label: 'Somalia',
|
||||
value: 'SO',
|
||||
},
|
||||
{
|
||||
label: 'South Africa',
|
||||
value: 'ZA',
|
||||
},
|
||||
{
|
||||
label: 'South Georgia and the South Sandwich Islands',
|
||||
value: 'GS',
|
||||
},
|
||||
{
|
||||
label: 'Spain',
|
||||
value: 'ES',
|
||||
},
|
||||
{
|
||||
label: 'Sri Lanka',
|
||||
value: 'LK',
|
||||
},
|
||||
{
|
||||
label: 'Sudan',
|
||||
value: 'SD',
|
||||
},
|
||||
{
|
||||
label: 'Suriname',
|
||||
value: 'SR',
|
||||
},
|
||||
{
|
||||
label: 'Svalbard and Jan Mayen',
|
||||
value: 'SJ',
|
||||
},
|
||||
{
|
||||
label: 'Swaziland',
|
||||
value: 'SZ',
|
||||
},
|
||||
{
|
||||
label: 'Sweden',
|
||||
value: 'SE',
|
||||
},
|
||||
{
|
||||
label: 'Switzerland',
|
||||
value: 'CH',
|
||||
},
|
||||
{
|
||||
label: 'Syrian Arab Republic',
|
||||
value: 'SY',
|
||||
},
|
||||
{
|
||||
label: 'Taiwan',
|
||||
value: 'TW',
|
||||
},
|
||||
{
|
||||
label: 'Tajikistan',
|
||||
value: 'TJ',
|
||||
},
|
||||
{
|
||||
label: 'Tanzania, United Republic of',
|
||||
value: 'TZ',
|
||||
},
|
||||
{
|
||||
label: 'Thailand',
|
||||
value: 'TH',
|
||||
},
|
||||
{
|
||||
label: 'Timor-Leste',
|
||||
value: 'TL',
|
||||
},
|
||||
{
|
||||
label: 'Togo',
|
||||
value: 'TG',
|
||||
},
|
||||
{
|
||||
label: 'Tokelau',
|
||||
value: 'TK',
|
||||
},
|
||||
{
|
||||
label: 'Tonga',
|
||||
value: 'TO',
|
||||
},
|
||||
{
|
||||
label: 'Trinidad and Tobago',
|
||||
value: 'TT',
|
||||
},
|
||||
{
|
||||
label: 'Tunisia',
|
||||
value: 'TN',
|
||||
},
|
||||
{
|
||||
label: 'Turkey',
|
||||
value: 'TR',
|
||||
},
|
||||
{
|
||||
label: 'Turkmenistan',
|
||||
value: 'TM',
|
||||
},
|
||||
{
|
||||
label: 'Turks and Caicos Islands',
|
||||
value: 'TC',
|
||||
},
|
||||
{
|
||||
label: 'Tuvalu',
|
||||
value: 'TV',
|
||||
},
|
||||
{
|
||||
label: 'Uganda',
|
||||
value: 'UG',
|
||||
},
|
||||
{
|
||||
label: 'Ukraine',
|
||||
value: 'UA',
|
||||
},
|
||||
{
|
||||
label: 'United Arab Emirates',
|
||||
value: 'AE',
|
||||
},
|
||||
{
|
||||
label: 'United Kingdom',
|
||||
value: 'GB',
|
||||
},
|
||||
{
|
||||
label: 'United States',
|
||||
value: 'US',
|
||||
},
|
||||
{
|
||||
label: 'United States Minor Outlying Islands',
|
||||
value: 'UM',
|
||||
},
|
||||
{
|
||||
label: 'Uruguay',
|
||||
value: 'UY',
|
||||
},
|
||||
{
|
||||
label: 'Uzbekistan',
|
||||
value: 'UZ',
|
||||
},
|
||||
{
|
||||
label: 'Vanuatu',
|
||||
value: 'VU',
|
||||
},
|
||||
{
|
||||
label: 'Venezuela',
|
||||
value: 'VE',
|
||||
},
|
||||
{
|
||||
label: 'Viet Nam',
|
||||
value: 'VN',
|
||||
},
|
||||
{
|
||||
label: 'Virgin Islands, British',
|
||||
value: 'VG',
|
||||
},
|
||||
{
|
||||
label: 'Virgin Islands, U.S.',
|
||||
value: 'VI',
|
||||
},
|
||||
{
|
||||
label: 'Wallis and Futuna',
|
||||
value: 'WF',
|
||||
},
|
||||
{
|
||||
label: 'Western Sahara',
|
||||
value: 'EH',
|
||||
},
|
||||
{
|
||||
label: 'Yemen',
|
||||
value: 'YE',
|
||||
},
|
||||
{
|
||||
label: 'Zambia',
|
||||
value: 'ZM',
|
||||
},
|
||||
{
|
||||
label: 'Zimbabwe',
|
||||
value: 'ZW',
|
||||
},
|
||||
]
|
||||
@@ -1,38 +0,0 @@
|
||||
import type { EmailField } from '@payloadcms/plugin-form-builder/types'
|
||||
import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form'
|
||||
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import React from 'react'
|
||||
|
||||
import { Error } from '../Error'
|
||||
import { Width } from '../Width'
|
||||
|
||||
export const Email: React.FC<
|
||||
EmailField & {
|
||||
errors: Partial<FieldErrorsImpl>
|
||||
register: UseFormRegister<FieldValues>
|
||||
}
|
||||
> = ({ name, defaultValue, errors, label, register, required, width }) => {
|
||||
return (
|
||||
<Width width={width}>
|
||||
<Label htmlFor={name}>
|
||||
{label}
|
||||
|
||||
{required && (
|
||||
<span className="required">
|
||||
* <span className="sr-only">(required)</span>
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
<Input
|
||||
defaultValue={defaultValue}
|
||||
id={name}
|
||||
type="text"
|
||||
{...register(name, { pattern: /^\S[^\s@]*@\S+$/, required })}
|
||||
/>
|
||||
|
||||
{errors[name] && <Error name={name} />}
|
||||
</Width>
|
||||
)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { useFormContext } from 'react-hook-form'
|
||||
|
||||
export const Error = ({ name }: { name: string }) => {
|
||||
const {
|
||||
formState: { errors },
|
||||
} = useFormContext()
|
||||
return (
|
||||
<div className="mt-2 text-red-500 text-sm">
|
||||
{(errors[name]?.message as string) || 'This field is required'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import RichText from '@/components/RichText'
|
||||
import React from 'react'
|
||||
|
||||
import { Width } from '../Width'
|
||||
import { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
export const Message: React.FC<{ message: SerializedEditorState }> = ({ message }) => {
|
||||
return (
|
||||
<Width className="my-12" width="100">
|
||||
{message && <RichText data={message} />}
|
||||
</Width>
|
||||
)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import type { TextField } from '@payloadcms/plugin-form-builder/types'
|
||||
import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form'
|
||||
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import React from 'react'
|
||||
|
||||
import { Error } from '../Error'
|
||||
import { Width } from '../Width'
|
||||
export const Number: React.FC<
|
||||
TextField & {
|
||||
errors: Partial<FieldErrorsImpl>
|
||||
register: UseFormRegister<FieldValues>
|
||||
}
|
||||
> = ({ name, defaultValue, errors, label, register, required, width }) => {
|
||||
return (
|
||||
<Width width={width}>
|
||||
<Label htmlFor={name}>
|
||||
{label}
|
||||
|
||||
{required && (
|
||||
<span className="required">
|
||||
* <span className="sr-only">(required)</span>
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
<Input
|
||||
defaultValue={defaultValue}
|
||||
id={name}
|
||||
type="number"
|
||||
{...register(name, { required })}
|
||||
/>
|
||||
{errors[name] && <Error name={name} />}
|
||||
</Width>
|
||||
)
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import type { SelectField } from '@payloadcms/plugin-form-builder/types'
|
||||
import type { Control, FieldErrorsImpl } from 'react-hook-form'
|
||||
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select as SelectComponent,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import React from 'react'
|
||||
import { Controller } from 'react-hook-form'
|
||||
|
||||
import { Error } from '../Error'
|
||||
import { Width } from '../Width'
|
||||
|
||||
export const Select: React.FC<
|
||||
SelectField & {
|
||||
control: Control
|
||||
errors: Partial<FieldErrorsImpl>
|
||||
}
|
||||
> = ({ name, control, errors, label, options, required, width, defaultValue }) => {
|
||||
return (
|
||||
<Width width={width}>
|
||||
<Label htmlFor={name}>
|
||||
{label}
|
||||
{required && (
|
||||
<span className="required">
|
||||
* <span className="sr-only">(required)</span>
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue={defaultValue}
|
||||
name={name}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const controlledValue = options.find((t) => t.value === value)
|
||||
|
||||
return (
|
||||
<SelectComponent onValueChange={(val) => onChange(val)} value={controlledValue?.value}>
|
||||
<SelectTrigger className="w-full" id={name}>
|
||||
<SelectValue placeholder={label} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map(({ label, value }) => {
|
||||
return (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
)
|
||||
})}
|
||||
</SelectContent>
|
||||
</SelectComponent>
|
||||
)
|
||||
}}
|
||||
rules={{ required }}
|
||||
/>
|
||||
{errors[name] && <Error name={name} />}
|
||||
</Width>
|
||||
)
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import type { StateField } from '@payloadcms/plugin-form-builder/types'
|
||||
import type { Control, FieldErrorsImpl } from 'react-hook-form'
|
||||
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import React from 'react'
|
||||
import { Controller } from 'react-hook-form'
|
||||
|
||||
import { Error } from '../Error'
|
||||
import { Width } from '../Width'
|
||||
import { stateOptions } from './options'
|
||||
|
||||
export const State: React.FC<
|
||||
StateField & {
|
||||
control: Control
|
||||
errors: Partial<FieldErrorsImpl>
|
||||
}
|
||||
> = ({ name, control, errors, label, required, width }) => {
|
||||
return (
|
||||
<Width width={width}>
|
||||
<Label htmlFor={name}>
|
||||
{label}
|
||||
{required && (
|
||||
<span className="required">
|
||||
* <span className="sr-only">(required)</span>
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name={name}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const controlledValue = stateOptions.find((t) => t.value === value)
|
||||
|
||||
return (
|
||||
<Select onValueChange={(val) => onChange(val)} value={controlledValue?.value}>
|
||||
<SelectTrigger className="w-full" id={name}>
|
||||
<SelectValue placeholder={label} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{stateOptions.map(({ label, value }) => {
|
||||
return (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
)
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
}}
|
||||
rules={{ required }}
|
||||
/>
|
||||
{errors[name] && <Error name={name} />}
|
||||
</Width>
|
||||
)
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
export const stateOptions = [
|
||||
{ label: 'Alabama', value: 'AL' },
|
||||
{ label: 'Alaska', value: 'AK' },
|
||||
{ label: 'Arizona', value: 'AZ' },
|
||||
{ label: 'Arkansas', value: 'AR' },
|
||||
{ label: 'California', value: 'CA' },
|
||||
{ label: 'Colorado', value: 'CO' },
|
||||
{ label: 'Connecticut', value: 'CT' },
|
||||
{ label: 'Delaware', value: 'DE' },
|
||||
{ label: 'Florida', value: 'FL' },
|
||||
{ label: 'Georgia', value: 'GA' },
|
||||
{ label: 'Hawaii', value: 'HI' },
|
||||
{ label: 'Idaho', value: 'ID' },
|
||||
{ label: 'Illinois', value: 'IL' },
|
||||
{ label: 'Indiana', value: 'IN' },
|
||||
{ label: 'Iowa', value: 'IA' },
|
||||
{ label: 'Kansas', value: 'KS' },
|
||||
{ label: 'Kentucky', value: 'KY' },
|
||||
{ label: 'Louisiana', value: 'LA' },
|
||||
{ label: 'Maine', value: 'ME' },
|
||||
{ label: 'Maryland', value: 'MD' },
|
||||
{ label: 'Massachusetts', value: 'MA' },
|
||||
{ label: 'Michigan', value: 'MI' },
|
||||
{ label: 'Minnesota', value: 'MN' },
|
||||
{ label: 'Mississippi', value: 'MS' },
|
||||
{ label: 'Missouri', value: 'MO' },
|
||||
{ label: 'Montana', value: 'MT' },
|
||||
{ label: 'Nebraska', value: 'NE' },
|
||||
{ label: 'Nevada', value: 'NV' },
|
||||
{ label: 'New Hampshire', value: 'NH' },
|
||||
{ label: 'New Jersey', value: 'NJ' },
|
||||
{ label: 'New Mexico', value: 'NM' },
|
||||
{ label: 'New York', value: 'NY' },
|
||||
{ label: 'North Carolina', value: 'NC' },
|
||||
{ label: 'North Dakota', value: 'ND' },
|
||||
{ label: 'Ohio', value: 'OH' },
|
||||
{ label: 'Oklahoma', value: 'OK' },
|
||||
{ label: 'Oregon', value: 'OR' },
|
||||
{ label: 'Pennsylvania', value: 'PA' },
|
||||
{ label: 'Rhode Island', value: 'RI' },
|
||||
{ label: 'South Carolina', value: 'SC' },
|
||||
{ label: 'South Dakota', value: 'SD' },
|
||||
{ label: 'Tennessee', value: 'TN' },
|
||||
{ label: 'Texas', value: 'TX' },
|
||||
{ label: 'Utah', value: 'UT' },
|
||||
{ label: 'Vermont', value: 'VT' },
|
||||
{ label: 'Virginia', value: 'VA' },
|
||||
{ label: 'Washington', value: 'WA' },
|
||||
{ label: 'West Virginia', value: 'WV' },
|
||||
{ label: 'Wisconsin', value: 'WI' },
|
||||
{ label: 'Wyoming', value: 'WY' },
|
||||
]
|
||||
@@ -1,32 +0,0 @@
|
||||
import type { TextField } from '@payloadcms/plugin-form-builder/types'
|
||||
import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form'
|
||||
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import React from 'react'
|
||||
|
||||
import { Error } from '../Error'
|
||||
import { Width } from '../Width'
|
||||
|
||||
export const Text: React.FC<
|
||||
TextField & {
|
||||
errors: Partial<FieldErrorsImpl>
|
||||
register: UseFormRegister<FieldValues>
|
||||
}
|
||||
> = ({ name, defaultValue, errors, label, register, required, width }) => {
|
||||
return (
|
||||
<Width width={width}>
|
||||
<Label htmlFor={name}>
|
||||
{label}
|
||||
|
||||
{required && (
|
||||
<span className="required">
|
||||
* <span className="sr-only">(required)</span>
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
<Input defaultValue={defaultValue} id={name} type="text" {...register(name, { required })} />
|
||||
{errors[name] && <Error name={name} />}
|
||||
</Width>
|
||||
)
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import type { TextField } from '@payloadcms/plugin-form-builder/types'
|
||||
import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form'
|
||||
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea as TextAreaComponent } from '@/components/ui/textarea'
|
||||
import React from 'react'
|
||||
|
||||
import { Error } from '../Error'
|
||||
import { Width } from '../Width'
|
||||
|
||||
export const Textarea: React.FC<
|
||||
TextField & {
|
||||
errors: Partial<FieldErrorsImpl>
|
||||
register: UseFormRegister<FieldValues>
|
||||
rows?: number
|
||||
}
|
||||
> = ({ name, defaultValue, errors, label, register, required, rows = 3, width }) => {
|
||||
return (
|
||||
<Width width={width}>
|
||||
<Label htmlFor={name}>
|
||||
{label}
|
||||
|
||||
{required && (
|
||||
<span className="required">
|
||||
* <span className="sr-only">(required)</span>
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
|
||||
<TextAreaComponent
|
||||
defaultValue={defaultValue}
|
||||
id={name}
|
||||
rows={rows}
|
||||
{...register(name, { required: required })}
|
||||
/>
|
||||
|
||||
{errors[name] && <Error name={name} />}
|
||||
</Width>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import * as React from 'react'
|
||||
|
||||
export const Width: React.FC<{
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
width?: number | string
|
||||
}> = ({ children, className, width }) => {
|
||||
return (
|
||||
<div className={className} style={{ maxWidth: width ? `${width}%` : undefined }}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import type { Block } from 'payload'
|
||||
|
||||
import {
|
||||
FixedToolbarFeature,
|
||||
HeadingFeature,
|
||||
InlineToolbarFeature,
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
export const FormBlock: Block = {
|
||||
slug: 'formBlock',
|
||||
interfaceName: 'FormBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'form',
|
||||
type: 'relationship',
|
||||
relationTo: 'forms',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'enableIntro',
|
||||
type: 'checkbox',
|
||||
label: 'Enable Intro Content',
|
||||
},
|
||||
{
|
||||
name: 'introContent',
|
||||
type: 'richText',
|
||||
admin: {
|
||||
condition: (_, { enableIntro }) => Boolean(enableIntro),
|
||||
},
|
||||
editor: lexicalEditor({
|
||||
features: ({ rootFeatures }) => {
|
||||
return [
|
||||
...rootFeatures,
|
||||
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
|
||||
FixedToolbarFeature(),
|
||||
InlineToolbarFeature(),
|
||||
]
|
||||
},
|
||||
}),
|
||||
label: 'Intro Content',
|
||||
},
|
||||
],
|
||||
graphQL: {
|
||||
singularName: 'FormBlock',
|
||||
},
|
||||
labels: {
|
||||
plural: 'Form Blocks',
|
||||
singular: 'Form Block',
|
||||
},
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Checkbox } from './Checkbox'
|
||||
import { Country } from './Country'
|
||||
import { Email } from './Email'
|
||||
import { Message } from './Message'
|
||||
import { Number } from './Number'
|
||||
import { Select } from './Select'
|
||||
import { State } from './State'
|
||||
import { Text } from './Text'
|
||||
import { Textarea } from './Textarea'
|
||||
|
||||
export const fields = {
|
||||
checkbox: Checkbox,
|
||||
country: Country,
|
||||
email: Email,
|
||||
message: Message,
|
||||
number: Number,
|
||||
select: Select,
|
||||
state: State,
|
||||
text: Text,
|
||||
textarea: Textarea,
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import clsx from 'clsx'
|
||||
import React from 'react'
|
||||
import RichText from '@/components/RichText'
|
||||
|
||||
import type { Post } from '@/payload-types'
|
||||
|
||||
import { Card } from '../../components/Card'
|
||||
import { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
export type RelatedPostsProps = {
|
||||
className?: string
|
||||
docs?: Post[]
|
||||
introContent?: SerializedEditorState
|
||||
}
|
||||
|
||||
export const RelatedPosts: React.FC<RelatedPostsProps> = (props) => {
|
||||
const { className, docs, introContent } = props
|
||||
|
||||
return (
|
||||
<div className={clsx('lg:container', className)}>
|
||||
{introContent && <RichText data={introContent} enableGutter={false} />}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-8 items-stretch">
|
||||
{docs?.map((doc, index) => {
|
||||
if (typeof doc === 'string') return null
|
||||
|
||||
return <Card key={index} doc={doc} relationTo="posts" showCategories />
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import type { Page } from '@/payload-types'
|
||||
|
||||
import { ArchiveBlock } from '@/blocks/ArchiveBlock/Component'
|
||||
import { CallToActionBlock } from '@/blocks/CallToAction/Component'
|
||||
import { ContentBlock } from '@/blocks/Content/Component'
|
||||
import { FormBlock } from '@/blocks/Form/Component'
|
||||
import { MediaBlock } from '@/blocks/MediaBlock/Component'
|
||||
|
||||
const blockComponents = {
|
||||
archive: ArchiveBlock,
|
||||
content: ContentBlock,
|
||||
cta: CallToActionBlock,
|
||||
formBlock: FormBlock,
|
||||
mediaBlock: MediaBlock,
|
||||
}
|
||||
|
||||
export const RenderBlocks: React.FC<{
|
||||
blocks: Page['layout'][0][]
|
||||
}> = (props) => {
|
||||
const { blocks } = props
|
||||
|
||||
const hasBlocks = blocks && Array.isArray(blocks) && blocks.length > 0
|
||||
|
||||
if (hasBlocks) {
|
||||
return (
|
||||
<Fragment>
|
||||
{blocks.map((block, index) => {
|
||||
const { blockType } = block
|
||||
|
||||
if (blockType && blockType in blockComponents) {
|
||||
const Block = blockComponents[blockType]
|
||||
|
||||
if (Block) {
|
||||
return (
|
||||
<div className="my-16" key={index}>
|
||||
{/* @ts-expect-error there may be some mismatch between the expected types here */}
|
||||
<Block {...block} disableInnerContainer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
return null
|
||||
})}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CollectionConfig } from 'payload'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Authors: CollectionConfig = {
|
||||
slug: 'authors',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CollectionConfig } from 'payload'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Categories: CollectionConfig = {
|
||||
slug: 'categories',
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { CollectionAfterChangeHook, CollectionAfterDeleteHook } from 'payload'
|
||||
|
||||
import { revalidatePath, revalidateTag } from 'next/cache'
|
||||
// Avoid importing next/cache at module scope so migrations can run in a plain Node env
|
||||
|
||||
import type { Customer } from '../../../payload-types'
|
||||
|
||||
export const revalidateCustomer: CollectionAfterChangeHook<Customer> = ({
|
||||
export const revalidateCustomer: CollectionAfterChangeHook<Customer> = async ({
|
||||
doc,
|
||||
previousDoc,
|
||||
req: { payload, context },
|
||||
@@ -14,9 +14,13 @@ export const revalidateCustomer: CollectionAfterChangeHook<Customer> = ({
|
||||
const path = `/customers/${doc.slug}`
|
||||
|
||||
payload.logger.info(`Revalidating event at path: ${path}`)
|
||||
|
||||
revalidatePath(path)
|
||||
revalidateTag('customers-sitemap')
|
||||
try {
|
||||
const { revalidatePath, revalidateTag } = await import('next/cache')
|
||||
revalidatePath(path)
|
||||
revalidateTag('customers-sitemap')
|
||||
} catch {
|
||||
// no-op when not running inside Next runtime (e.g., during payload migrate)
|
||||
}
|
||||
}
|
||||
|
||||
// If the event was previously published, we need to revalidate the old path
|
||||
@@ -25,22 +29,27 @@ export const revalidateCustomer: CollectionAfterChangeHook<Customer> = ({
|
||||
|
||||
payload.logger.info(`Revalidating old event at path: ${oldPath}`)
|
||||
|
||||
revalidatePath(oldPath)
|
||||
revalidateTag('customers-sitemap')
|
||||
try {
|
||||
const { revalidatePath, revalidateTag } = await import('next/cache')
|
||||
revalidatePath(oldPath)
|
||||
revalidateTag('customers-sitemap')
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
export const revalidateDelete: CollectionAfterDeleteHook<Customer> = ({
|
||||
export const revalidateDelete: CollectionAfterDeleteHook<Customer> = async ({
|
||||
doc,
|
||||
req: { context },
|
||||
}) => {
|
||||
if (!context.disableRevalidate) {
|
||||
const path = `/customers/${doc?.slug}`
|
||||
|
||||
revalidatePath(path)
|
||||
revalidateTag('customers-sitemap')
|
||||
try {
|
||||
const { revalidatePath, revalidateTag } = await import('next/cache')
|
||||
revalidatePath(path)
|
||||
revalidateTag('customers-sitemap')
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return doc
|
||||
|
||||
@@ -9,15 +9,15 @@ import {
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { isAnyone } from '@/access/isAnyone'
|
||||
import { isAuthenticated } from '@/access/isAuthenticated'
|
||||
import { isAnyone } from '../../access/isAnyone.ts'
|
||||
import { isAuthenticated } from '../../access/isAuthenticated.ts'
|
||||
|
||||
import { Banner } from '../../blocks/Banner/config'
|
||||
import { Code } from '../../blocks/Code/config'
|
||||
import { MediaBlock } from '../../blocks/MediaBlock/config'
|
||||
import { Quote } from '../../blocks/Quote/config'
|
||||
import { YouTube } from '../../blocks/YouTube/config'
|
||||
import { revalidateDelete, revalidateCustomer } from './hooks/revalidateCustomer'
|
||||
import { Banner } from '../../blocks/Banner/config.ts'
|
||||
import { Code } from '../../blocks/Code/config.ts'
|
||||
import { MediaBlock } from '../../blocks/MediaBlock/config.ts'
|
||||
import { Quote } from '../../blocks/Quote/config.ts'
|
||||
import { YouTube } from '../../blocks/YouTube/config.ts'
|
||||
import { revalidateDelete, revalidateCustomer } from './hooks/revalidateCustomer.ts'
|
||||
|
||||
import {
|
||||
MetaDescriptionField,
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
OverviewField,
|
||||
PreviewField,
|
||||
} from '@payloadcms/plugin-seo/fields'
|
||||
import { slugField } from '@/fields/slug'
|
||||
import { slugField } from '../../fields/slug/index.ts'
|
||||
|
||||
const industryOptions = [
|
||||
{ label: 'Healthcare', value: 'healthcare' },
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { CollectionAfterChangeHook, CollectionAfterDeleteHook } from 'payload'
|
||||
|
||||
import { revalidatePath, revalidateTag } from 'next/cache'
|
||||
// Avoid importing next/cache at module scope so migrations can run in a plain Node env
|
||||
|
||||
import type { Event } from '../../../payload-types'
|
||||
|
||||
export const revalidateEvent: CollectionAfterChangeHook<Event> = ({
|
||||
export const revalidateEvent: CollectionAfterChangeHook<Event> = async ({
|
||||
doc,
|
||||
previousDoc,
|
||||
req: { payload, context },
|
||||
@@ -14,9 +14,11 @@ export const revalidateEvent: CollectionAfterChangeHook<Event> = ({
|
||||
const path = `/events/${doc.slug}`
|
||||
|
||||
payload.logger.info(`Revalidating event at path: ${path}`)
|
||||
|
||||
revalidatePath(path)
|
||||
revalidateTag('events-sitemap')
|
||||
try {
|
||||
const { revalidatePath, revalidateTag } = await import('next/cache')
|
||||
revalidatePath(path)
|
||||
revalidateTag('events-sitemap')
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// If the event was previously published, we need to revalidate the old path
|
||||
@@ -25,19 +27,27 @@ export const revalidateEvent: CollectionAfterChangeHook<Event> = ({
|
||||
|
||||
payload.logger.info(`Revalidating old event at path: ${oldPath}`)
|
||||
|
||||
revalidatePath(oldPath)
|
||||
revalidateTag('events-sitemap')
|
||||
try {
|
||||
const { revalidatePath, revalidateTag } = await import('next/cache')
|
||||
revalidatePath(oldPath)
|
||||
revalidateTag('events-sitemap')
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
export const revalidateDelete: CollectionAfterDeleteHook<Event> = ({ doc, req: { context } }) => {
|
||||
export const revalidateDelete: CollectionAfterDeleteHook<Event> = async ({
|
||||
doc,
|
||||
req: { context },
|
||||
}) => {
|
||||
if (!context.disableRevalidate) {
|
||||
const path = `/events/${doc?.slug}`
|
||||
|
||||
revalidatePath(path)
|
||||
revalidateTag('events-sitemap')
|
||||
try {
|
||||
const { revalidatePath, revalidateTag } = await import('next/cache')
|
||||
revalidatePath(path)
|
||||
revalidateTag('events-sitemap')
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return doc
|
||||
|
||||
@@ -9,15 +9,15 @@ import {
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { isAnyone } from '@/access/isAnyone'
|
||||
import { isAuthenticated } from '@/access/isAuthenticated'
|
||||
import { isAnyone } from '../../access/isAnyone.ts'
|
||||
import { isAuthenticated } from '../../access/isAuthenticated.ts'
|
||||
|
||||
import { Banner } from '@/blocks/Banner/config'
|
||||
import { Code } from '@/blocks/Code/config'
|
||||
import { MediaBlock } from '@/blocks/MediaBlock/config'
|
||||
import { Quote } from '@/blocks/Quote/config'
|
||||
import { YouTube } from '@/blocks/YouTube/config'
|
||||
import { revalidateDelete, revalidateEvent } from './hooks/revalidateEvent'
|
||||
import { Banner } from '../../blocks/Banner/config.ts'
|
||||
import { Code } from '../../blocks/Code/config.ts'
|
||||
import { MediaBlock } from '../../blocks/MediaBlock/config.ts'
|
||||
import { Quote } from '../../blocks/Quote/config.ts'
|
||||
import { YouTube } from '../../blocks/YouTube/config.ts'
|
||||
import { revalidateDelete, revalidateEvent } from './hooks/revalidateEvent.ts'
|
||||
|
||||
import {
|
||||
MetaDescriptionField,
|
||||
@@ -26,8 +26,8 @@ import {
|
||||
OverviewField,
|
||||
PreviewField,
|
||||
} from '@payloadcms/plugin-seo/fields'
|
||||
import { slugField } from '@/fields/slug'
|
||||
import { timezoneOptions } from '../../utilities/timezones'
|
||||
import { slugField } from '../../fields/slug/index.ts'
|
||||
import { timezoneOptions } from '../../utilities/timezones.ts'
|
||||
|
||||
const eventTypeOptions = [
|
||||
{ label: 'Conference', value: 'conference' },
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { isAnyone } from '@/access/isAnyone'
|
||||
import { isAuthenticated } from '@/access/isAuthenticated'
|
||||
import { isAnyone } from '../access/isAnyone.ts'
|
||||
import { isAuthenticated } from '../access/isAuthenticated.ts'
|
||||
|
||||
export const Media: CollectionConfig = {
|
||||
slug: 'media',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CollectionAfterReadHook } from 'payload'
|
||||
import { Author } from 'src/payload-types'
|
||||
import type { Author } from '../../../payload-types'
|
||||
|
||||
export const populateAuthors: CollectionAfterReadHook = async ({ doc, req: { payload } }) => {
|
||||
if (doc?.authors && doc?.authors?.length > 0) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { CollectionAfterChangeHook, CollectionAfterDeleteHook } from 'payload'
|
||||
|
||||
import { revalidatePath, revalidateTag } from 'next/cache'
|
||||
// Avoid importing next/cache at module scope so migrations can run in a plain Node env
|
||||
|
||||
import type { Post } from '../../../payload-types'
|
||||
|
||||
export const revalidatePost: CollectionAfterChangeHook<Post> = ({
|
||||
export const revalidatePost: CollectionAfterChangeHook<Post> = async ({
|
||||
doc,
|
||||
previousDoc,
|
||||
req: { payload, context },
|
||||
@@ -14,9 +14,11 @@ export const revalidatePost: CollectionAfterChangeHook<Post> = ({
|
||||
const path = `/posts/${doc.slug}`
|
||||
|
||||
payload.logger.info(`Revalidating post at path: ${path}`)
|
||||
|
||||
revalidatePath(path)
|
||||
revalidateTag('posts-sitemap')
|
||||
try {
|
||||
const { revalidatePath, revalidateTag } = await import('next/cache')
|
||||
revalidatePath(path)
|
||||
revalidateTag('posts-sitemap')
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// If the post was previously published, we need to revalidate the old path
|
||||
@@ -25,19 +27,27 @@ export const revalidatePost: CollectionAfterChangeHook<Post> = ({
|
||||
|
||||
payload.logger.info(`Revalidating old post at path: ${oldPath}`)
|
||||
|
||||
revalidatePath(oldPath)
|
||||
revalidateTag('posts-sitemap')
|
||||
try {
|
||||
const { revalidatePath, revalidateTag } = await import('next/cache')
|
||||
revalidatePath(oldPath)
|
||||
revalidateTag('posts-sitemap')
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
export const revalidateDelete: CollectionAfterDeleteHook<Post> = ({ doc, req: { context } }) => {
|
||||
export const revalidateDelete: CollectionAfterDeleteHook<Post> = async ({
|
||||
doc,
|
||||
req: { context },
|
||||
}) => {
|
||||
if (!context.disableRevalidate) {
|
||||
const path = `/posts/${doc?.slug}`
|
||||
|
||||
revalidatePath(path)
|
||||
revalidateTag('posts-sitemap')
|
||||
try {
|
||||
const { revalidatePath, revalidateTag } = await import('next/cache')
|
||||
revalidatePath(path)
|
||||
revalidateTag('posts-sitemap')
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return doc
|
||||
|
||||
@@ -9,16 +9,16 @@ import {
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { isAnyone } from '@/access/isAnyone'
|
||||
import { isAuthenticated } from '@/access/isAuthenticated'
|
||||
import { isAnyone } from '../../access/isAnyone.ts'
|
||||
import { isAuthenticated } from '../../access/isAuthenticated.ts'
|
||||
|
||||
import { Banner } from '@/blocks/Banner/config'
|
||||
import { Code } from '@/blocks/Code/config'
|
||||
import { MediaBlock } from '@/blocks/MediaBlock/config'
|
||||
import { Quote } from '@/blocks/Quote/config'
|
||||
import { YouTube } from '@/blocks/YouTube/config'
|
||||
import { populateAuthors } from './hooks/populateAuthors'
|
||||
import { revalidateDelete, revalidatePost } from './hooks/revalidatePost'
|
||||
import { Banner } from '../../blocks/Banner/config.ts'
|
||||
import { Code } from '../../blocks/Code/config.ts'
|
||||
import { MediaBlock } from '../../blocks/MediaBlock/config.ts'
|
||||
import { Quote } from '../../blocks/Quote/config.ts'
|
||||
import { YouTube } from '../../blocks/YouTube/config.ts'
|
||||
import { populateAuthors } from './hooks/populateAuthors.ts'
|
||||
import { revalidateDelete, revalidatePost } from './hooks/revalidatePost.ts'
|
||||
|
||||
import {
|
||||
MetaDescriptionField,
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
OverviewField,
|
||||
PreviewField,
|
||||
} from '@payloadcms/plugin-seo/fields'
|
||||
import { slugField } from '@/fields/slug'
|
||||
import { slugField } from '../../fields/slug/index.ts'
|
||||
|
||||
const launchweekOptions = [
|
||||
{ label: '6', value: '6' },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CollectionConfig } from 'payload'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Tags: CollectionConfig = {
|
||||
slug: 'tags',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import { isAdmin, isAdminFieldLevel } from '@/access/isAdmin'
|
||||
import { isAdminOrSelf } from '@/access/isAdminOrSelf'
|
||||
import { isAdmin, isAdminFieldLevel } from '../access/isAdmin.ts'
|
||||
import { isAdminOrSelf } from '../access/isAdminOrSelf.ts'
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
|
||||
@@ -35,7 +35,7 @@ export const Card: React.FC<{
|
||||
'border border-border rounded-lg overflow-hidden bg-card hover:cursor-pointer',
|
||||
className
|
||||
)}
|
||||
ref={card.ref}
|
||||
ref={card.ref as React.Ref<HTMLDivElement>}
|
||||
>
|
||||
<div className="relative w-full ">
|
||||
{!metaImage && <div className="">No image</div>}
|
||||
@@ -48,15 +48,15 @@ export const Card: React.FC<{
|
||||
<div>
|
||||
{categories?.map((category, index) => {
|
||||
if (typeof category === 'object') {
|
||||
const { title: titleFromCategory } = category
|
||||
const { name: nameFromCategory } = category
|
||||
|
||||
const categoryTitle = titleFromCategory || 'Untitled category'
|
||||
const categoryName = nameFromCategory || 'Untitled category'
|
||||
|
||||
const isLast = index === categories.length - 1
|
||||
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
{categoryTitle}
|
||||
{categoryName}
|
||||
{!isLast && <Fragment>, </Fragment>}
|
||||
</Fragment>
|
||||
)
|
||||
@@ -71,7 +71,11 @@ export const Card: React.FC<{
|
||||
{titleToUse && (
|
||||
<div className="prose">
|
||||
<h3>
|
||||
<Link className="not-prose" href={href} ref={link.ref}>
|
||||
<Link
|
||||
className="not-prose"
|
||||
href={href}
|
||||
ref={link.ref as React.Ref<HTMLAnchorElement>}
|
||||
>
|
||||
{titleToUse}
|
||||
</Link>
|
||||
</h3>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { cn } from '@/utilities/ui'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
import type { Page, Post } from '@/payload-types'
|
||||
import type { Post } from '@/payload-types'
|
||||
|
||||
type CMSLinkType = {
|
||||
appearance?: 'inline' | ButtonProps['variant']
|
||||
@@ -12,8 +12,8 @@ type CMSLinkType = {
|
||||
label?: string | null
|
||||
newTab?: boolean | null
|
||||
reference?: {
|
||||
relationTo: 'pages' | 'posts'
|
||||
value: Page | Post | string | number
|
||||
relationTo: 'posts'
|
||||
value: Post | string | number
|
||||
} | null
|
||||
size?: ButtonProps['size'] | null
|
||||
type?: 'custom' | 'reference' | null
|
||||
@@ -35,9 +35,7 @@ export const CMSLink: React.FC<CMSLinkType> = (props) => {
|
||||
|
||||
const href =
|
||||
type === 'reference' && typeof reference?.value === 'object' && reference.value.slug
|
||||
? `${reference?.relationTo !== 'pages' ? `/${reference?.relationTo}` : ''}/${
|
||||
reference.value.slug
|
||||
}`
|
||||
? `/${reference.value.slug}`
|
||||
: url
|
||||
|
||||
if (!href) return null
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import type React from 'react'
|
||||
import type { Post } from '@/payload-types'
|
||||
|
||||
import { getCachedDocument } from '@/utilities/getDocument'
|
||||
import { getCachedRedirects } from '@/utilities/getRedirects'
|
||||
import { notFound, redirect } from 'next/navigation'
|
||||
|
||||
interface Props {
|
||||
disableNotFound?: boolean
|
||||
url: string
|
||||
}
|
||||
|
||||
/* This component helps us with SSR based dynamic redirects */
|
||||
export const PayloadRedirects: React.FC<Props> = async ({ disableNotFound, url }) => {
|
||||
const redirects = await getCachedRedirects()()
|
||||
|
||||
const redirectItem = redirects.find((redirect) => redirect.from === url)
|
||||
|
||||
if (redirectItem) {
|
||||
if (redirectItem.to?.url) {
|
||||
redirect(redirectItem.to.url)
|
||||
}
|
||||
|
||||
let redirectUrl: string
|
||||
|
||||
if (typeof redirectItem.to?.reference?.value === 'string') {
|
||||
const collection = redirectItem.to?.reference?.relationTo
|
||||
const id = redirectItem.to?.reference?.value
|
||||
|
||||
const document = (await getCachedDocument(collection, id)()) as Post
|
||||
redirectUrl = `${redirectItem.to?.reference?.relationTo !== 'pages' ? `/${redirectItem.to?.reference?.relationTo}` : ''}/${
|
||||
document?.slug
|
||||
}`
|
||||
} else {
|
||||
redirectUrl = `${redirectItem.to?.reference?.relationTo !== 'pages' ? `/${redirectItem.to?.reference?.relationTo}` : ''}/${
|
||||
typeof redirectItem.to?.reference?.value === 'object'
|
||||
? redirectItem.to?.reference?.value?.slug
|
||||
: ''
|
||||
}`
|
||||
}
|
||||
|
||||
if (redirectUrl) redirect(redirectUrl)
|
||||
}
|
||||
|
||||
if (disableNotFound) return null
|
||||
|
||||
notFound()
|
||||
}
|
||||
@@ -15,13 +15,11 @@ import { CodeBlock, CodeBlockProps } from '@/blocks/Code/Component'
|
||||
|
||||
import type {
|
||||
BannerBlock as BannerBlockProps,
|
||||
CallToActionBlock as CTABlockProps,
|
||||
MediaBlock as MediaBlockProps,
|
||||
QuoteBlock as QuoteBlockProps,
|
||||
YouTubeBlock as YouTubeBlockProps,
|
||||
} from '@/payload-types'
|
||||
import { BannerBlock } from '@/blocks/Banner/Component'
|
||||
import { CallToActionBlock } from '@/blocks/CallToAction/Component'
|
||||
import { QuoteBlock } from '@/blocks/Quote/Component'
|
||||
import { YouTubeBlock } from '@/blocks/YouTube/Component'
|
||||
import { cn } from '@/utilities/ui'
|
||||
@@ -29,12 +27,7 @@ import { cn } from '@/utilities/ui'
|
||||
type NodeTypes =
|
||||
| DefaultNodeTypes
|
||||
| SerializedBlockNode<
|
||||
| CTABlockProps
|
||||
| MediaBlockProps
|
||||
| BannerBlockProps
|
||||
| CodeBlockProps
|
||||
| QuoteBlockProps
|
||||
| YouTubeBlockProps
|
||||
MediaBlockProps | BannerBlockProps | CodeBlockProps | QuoteBlockProps | YouTubeBlockProps
|
||||
>
|
||||
|
||||
const internalDocToHref = ({ linkNode }: { linkNode: SerializedLinkNode }) => {
|
||||
@@ -62,7 +55,6 @@ const jsxConverters: JSXConvertersFunction<NodeTypes> = ({ defaultConverters })
|
||||
/>
|
||||
),
|
||||
code: ({ node }) => <CodeBlock className="col-start-2" {...node.fields} />,
|
||||
cta: ({ node }) => <CallToActionBlock {...node.fields} />,
|
||||
quote: ({ node }) => <QuoteBlock className="col-start-2" {...node.fields} />,
|
||||
youtube: ({ node }) => <YouTubeBlock className="col-start-2" {...node.fields} />,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Field, GroupField } from 'payload'
|
||||
|
||||
import deepMerge from '@/utilities/deepMerge'
|
||||
import deepMerge from '../utilities/deepMerge.ts'
|
||||
|
||||
export type LinkAppearances = 'default' | 'outline'
|
||||
|
||||
@@ -75,7 +75,7 @@ export const link: LinkType = ({ appearances, disableLabel = false, overrides =
|
||||
condition: (_, siblingData) => siblingData?.type === 'reference',
|
||||
},
|
||||
label: 'Document to link to',
|
||||
relationTo: ['pages', 'posts'],
|
||||
relationTo: ['posts'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { ArrayField, Field } from 'payload'
|
||||
|
||||
import type { LinkAppearances } from './link'
|
||||
import type { LinkAppearances } from './link.ts'
|
||||
|
||||
import deepMerge from '@/utilities/deepMerge'
|
||||
import { link } from './link'
|
||||
import deepMerge from '../utilities/deepMerge.ts'
|
||||
import { link } from './link.ts'
|
||||
|
||||
type LinkGroupType = (options?: {
|
||||
appearances?: LinkAppearances[] | false
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CheckboxField, TextField } from 'payload'
|
||||
|
||||
import { formatSlugHook } from './formatSlug'
|
||||
import { formatSlugHook } from './formatSlug.ts'
|
||||
|
||||
type Overrides = {
|
||||
slugOverrides?: Partial<TextField>
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+154
-221
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,15 @@
|
||||
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
||||
|
||||
export async function up({ db }: MigrateUpArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
INSERT INTO storage.buckets (id, name, public) VALUES ('cms', 'cms', false)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
`)
|
||||
}
|
||||
|
||||
export async function down({ db }: MigrateDownArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
DELETE FROM storage.objects WHERE bucket_id = 'cms';
|
||||
DELETE FROM storage.buckets WHERE id = 'cms';
|
||||
`)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,21 @@
|
||||
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
||||
|
||||
export async function up({ db }: MigrateUpArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
CREATE TABLE "users_sessions" (
|
||||
"_order" integer NOT NULL,
|
||||
"_parent_id" integer NOT NULL,
|
||||
"id" varchar PRIMARY KEY NOT NULL,
|
||||
"created_at" timestamp(3) with time zone,
|
||||
"expires_at" timestamp(3) with time zone NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE "users_sessions" ADD CONSTRAINT "users_sessions_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
CREATE INDEX "users_sessions_order_idx" ON "users_sessions" USING btree ("_order");
|
||||
CREATE INDEX "users_sessions_parent_id_idx" ON "users_sessions" USING btree ("_parent_id");`)
|
||||
}
|
||||
|
||||
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
DROP TABLE "users_sessions" CASCADE;`)
|
||||
}
|
||||
@@ -1,9 +1,21 @@
|
||||
import * as migration_20250529_103319 from './20250529_103319';
|
||||
import * as migration_20250619_155345 from './20250619_155345.ts';
|
||||
import * as migration_20250619_160549 from './20250619_160549.ts';
|
||||
import * as migration_20250808_145722_update_payload_migrations from './20250808_145722_update_payload_migrations.ts';
|
||||
|
||||
export const migrations = [
|
||||
{
|
||||
up: migration_20250529_103319.up,
|
||||
down: migration_20250529_103319.down,
|
||||
name: '20250529_103319'
|
||||
up: migration_20250619_155345.up,
|
||||
down: migration_20250619_155345.down,
|
||||
name: '20250619_155345',
|
||||
},
|
||||
{
|
||||
up: migration_20250619_160549.up,
|
||||
down: migration_20250619_160549.down,
|
||||
name: '20250619_160549',
|
||||
},
|
||||
{
|
||||
up: migration_20250808_145722_update_payload_migrations.up,
|
||||
down: migration_20250808_145722_update_payload_migrations.down,
|
||||
name: '20250808_145722_update_payload_migrations'
|
||||
},
|
||||
];
|
||||
@@ -987,6 +987,13 @@ export interface User {
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
sessions?:
|
||||
| {
|
||||
id: string;
|
||||
createdAt?: string | null;
|
||||
expiresAt: string;
|
||||
}[]
|
||||
| null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
@@ -1454,6 +1461,13 @@ export interface UsersSelect<T extends boolean = true> {
|
||||
hash?: T;
|
||||
loginAttempts?: T;
|
||||
lockUntil?: T;
|
||||
sessions?:
|
||||
| T
|
||||
| {
|
||||
id?: T;
|
||||
createdAt?: T;
|
||||
expiresAt?: T;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
|
||||
@@ -1,23 +1,78 @@
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
import path from 'path'
|
||||
import { buildConfig } from 'payload'
|
||||
import { fileURLToPath } from 'url'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import sharp from 'sharp'
|
||||
import { plugins } from './plugins'
|
||||
import { defaultLexical } from '@/fields/defaultLexical'
|
||||
import { getServerSideURL } from './utilities/getURL'
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
import { payloadCloudPlugin } from '@payloadcms/payload-cloud'
|
||||
import { nestedDocsPlugin } from '@payloadcms/plugin-nested-docs'
|
||||
import { seoPlugin } from '@payloadcms/plugin-seo'
|
||||
import { s3Storage } from '@payloadcms/storage-s3'
|
||||
import { buildConfig, type Plugin } from 'payload'
|
||||
import { defaultLexical } from './fields/defaultLexical.ts'
|
||||
import { getServerSideURL } from './utilities/getURL.ts'
|
||||
|
||||
import { Authors } from './collections/Authors'
|
||||
import { Categories } from './collections/Categories'
|
||||
import { Customers } from './collections/Customers'
|
||||
import { Events } from './collections/Events'
|
||||
import { Media } from './collections/Media'
|
||||
import { Posts } from './collections/Posts'
|
||||
import { Tags } from './collections/Tags'
|
||||
import { Users } from './collections/Users'
|
||||
import type { GenerateTitle, GenerateURL } from '@payloadcms/plugin-seo/types'
|
||||
import type { Customer, Event, Post } from './payload-types'
|
||||
|
||||
import { Authors } from './collections/Authors.ts'
|
||||
import { Categories } from './collections/Categories.ts'
|
||||
import { Customers } from './collections/Customers/index.ts'
|
||||
import { Events } from './collections/Events/index.ts'
|
||||
import { Media } from './collections/Media.ts'
|
||||
import { Posts } from './collections/Posts/index.ts'
|
||||
import { Tags } from './collections/Tags.ts'
|
||||
import { Users } from './collections/Users.ts'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
const siteName = 'Supabase'
|
||||
|
||||
const generateTitle: GenerateTitle<Post & Customer & Event> = ({ doc, collectionSlug }: any) => {
|
||||
switch (collectionSlug) {
|
||||
case 'customers':
|
||||
return `${doc.name} | ${siteName} Customer Stories`
|
||||
case 'events':
|
||||
return `${doc.title} | ${siteName} Events`
|
||||
case 'posts':
|
||||
return doc.title
|
||||
default:
|
||||
return `${doc.title} | ${siteName}`
|
||||
}
|
||||
}
|
||||
|
||||
const generateURL: GenerateURL<Post> = ({ doc }: any) => {
|
||||
const url = getServerSideURL()
|
||||
|
||||
return doc?.slug ? `${url}/${doc.slug}` : url
|
||||
}
|
||||
|
||||
const plugins: Plugin[] = [
|
||||
nestedDocsPlugin({
|
||||
collections: ['categories'],
|
||||
generateURL: (docs) => docs.reduce((url, doc) => `${url}/${doc.slug}`, ''),
|
||||
}),
|
||||
seoPlugin({
|
||||
generateTitle,
|
||||
generateURL,
|
||||
}),
|
||||
payloadCloudPlugin(),
|
||||
s3Storage({
|
||||
collections: {
|
||||
media: {
|
||||
prefix: 'media',
|
||||
},
|
||||
},
|
||||
bucket: process.env.S3_BUCKET || '',
|
||||
config: {
|
||||
forcePathStyle: true,
|
||||
credentials: {
|
||||
accessKeyId: process.env.S3_ACCESS_KEY_ID || '',
|
||||
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY || '',
|
||||
},
|
||||
region: process.env.S3_REGION,
|
||||
endpoint: process.env.S3_ENDPOINT,
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
||||
export default buildConfig({
|
||||
admin: {
|
||||
@@ -59,7 +114,7 @@ export default buildConfig({
|
||||
pool: {
|
||||
connectionString: process.env.DATABASE_URI || '',
|
||||
},
|
||||
schemaName: 'cms-payload',
|
||||
// schemaName: 'cms-payload',
|
||||
}),
|
||||
sharp,
|
||||
plugins,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
export default buildConfig({
|
||||
secret: process.env.PAYLOAD_SECRET,
|
||||
db: postgresAdapter({
|
||||
pool: {
|
||||
connectionString: process.env.DATABASE_URI || '',
|
||||
},
|
||||
}),
|
||||
})
|
||||
@@ -1,87 +0,0 @@
|
||||
import { payloadCloudPlugin } from '@payloadcms/payload-cloud'
|
||||
// import { formBuilderPlugin } from '@payloadcms/plugin-form-builder'
|
||||
import { nestedDocsPlugin } from '@payloadcms/plugin-nested-docs'
|
||||
import { seoPlugin } from '@payloadcms/plugin-seo'
|
||||
import { s3Storage } from '@payloadcms/storage-s3'
|
||||
import { Plugin } from 'payload'
|
||||
import { GenerateTitle, GenerateURL } from '@payloadcms/plugin-seo/types'
|
||||
// import { FixedToolbarFeature, HeadingFeature, lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { Customer, Event, Post } from '@/payload-types'
|
||||
import { getServerSideURL } from '@/utilities/getURL'
|
||||
|
||||
const siteName = 'Supabase'
|
||||
|
||||
const generateTitle: GenerateTitle<Post & Customer & Event> = ({ doc, collectionSlug }) => {
|
||||
switch (collectionSlug) {
|
||||
case 'customers':
|
||||
return `${doc.name} | ${siteName} Customer Stories`
|
||||
case 'events':
|
||||
return `${doc.title} | ${siteName} Events`
|
||||
case 'posts':
|
||||
return doc.title
|
||||
default:
|
||||
return `${doc.title} | ${siteName}`
|
||||
}
|
||||
}
|
||||
|
||||
const generateURL: GenerateURL<Post> = ({ doc }) => {
|
||||
const url = getServerSideURL()
|
||||
|
||||
return doc?.slug ? `${url}/${doc.slug}` : url
|
||||
}
|
||||
|
||||
export const plugins: Plugin[] = [
|
||||
nestedDocsPlugin({
|
||||
collections: ['categories'],
|
||||
generateURL: (docs) => docs.reduce((url, doc) => `${url}/${doc.slug}`, ''),
|
||||
}),
|
||||
seoPlugin({
|
||||
generateTitle,
|
||||
generateURL,
|
||||
}),
|
||||
// formBuilderPlugin({
|
||||
// fields: {
|
||||
// payment: false,
|
||||
// },
|
||||
// formOverrides: {
|
||||
// fields: ({ defaultFields }) => {
|
||||
// return defaultFields.map((field) => {
|
||||
// if ('name' in field && field.name === 'confirmationMessage') {
|
||||
// return {
|
||||
// ...field,
|
||||
// editor: lexicalEditor({
|
||||
// features: ({ rootFeatures }) => {
|
||||
// return [
|
||||
// ...rootFeatures,
|
||||
// FixedToolbarFeature(),
|
||||
// HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
|
||||
// ]
|
||||
// },
|
||||
// }),
|
||||
// }
|
||||
// }
|
||||
// return field
|
||||
// })
|
||||
// },
|
||||
// },
|
||||
// }),
|
||||
payloadCloudPlugin(),
|
||||
s3Storage({
|
||||
collections: {
|
||||
media: {
|
||||
prefix: 'media',
|
||||
},
|
||||
},
|
||||
bucket: process.env.S3_BUCKET || '',
|
||||
config: {
|
||||
forcePathStyle: true,
|
||||
credentials: {
|
||||
accessKeyId: process.env.S3_ACCESS_KEY_ID || '',
|
||||
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY || '',
|
||||
},
|
||||
region: process.env.S3_REGION,
|
||||
endpoint: process.env.S3_ENDPOINT,
|
||||
},
|
||||
}),
|
||||
]
|
||||
@@ -27,7 +27,11 @@ export const HeaderThemeProvider = ({ children }: { children: React.ReactNode })
|
||||
setThemeState(themeToSet)
|
||||
}, [])
|
||||
|
||||
return <HeaderThemeContext value={{ headerTheme, setHeaderTheme }}>{children}</HeaderThemeContext>
|
||||
return (
|
||||
<HeaderThemeContext.Provider value={{ headerTheme, setHeaderTheme }}>
|
||||
{children}
|
||||
</HeaderThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useHeaderTheme = (): ContextType => use(HeaderThemeContext)
|
||||
@@ -51,7 +51,7 @@ export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
setThemeState(themeToSet)
|
||||
}, [])
|
||||
|
||||
return <ThemeContext value={{ setTheme, theme }}>{children}</ThemeContext>
|
||||
return <ThemeContext.Provider value={{ setTheme, theme }}>{children}</ThemeContext.Provider>
|
||||
}
|
||||
|
||||
export const useTheme = (): ThemeContextType => use(ThemeContext)
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Theme } from './types'
|
||||
import type { Theme } from './types.ts'
|
||||
|
||||
export const themeLocalStorageKey = 'payload-theme'
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
'use client'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useDebounce } from '@/utilities/useDebounce'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
export const Search: React.FC = () => {
|
||||
const [value, setValue] = useState('')
|
||||
const router = useRouter()
|
||||
|
||||
const debouncedValue = useDebounce(value)
|
||||
|
||||
useEffect(() => {
|
||||
router.push(`/search${debouncedValue ? `?q=${debouncedValue}` : ''}`)
|
||||
}, [debouncedValue, router])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
<Label htmlFor="search" className="sr-only">
|
||||
Search
|
||||
</Label>
|
||||
<Input
|
||||
id="search"
|
||||
onChange={(event) => {
|
||||
setValue(event.target.value)
|
||||
}}
|
||||
placeholder="Search"
|
||||
/>
|
||||
<button type="submit" className="sr-only">
|
||||
submit
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { BeforeSync, DocToSync } from '@payloadcms/plugin-search/types'
|
||||
|
||||
export const beforeSyncWithSearch: BeforeSync = async ({ originalDoc, searchDoc }) => {
|
||||
const {
|
||||
doc: { relationTo: collection },
|
||||
} = searchDoc
|
||||
|
||||
const { slug, id, categories, title, meta } = originalDoc
|
||||
|
||||
const modifiedDoc: DocToSync = {
|
||||
...searchDoc,
|
||||
slug,
|
||||
meta: {
|
||||
...meta,
|
||||
title: meta?.title || title,
|
||||
image: meta?.image?.id || meta?.image,
|
||||
description: meta?.description,
|
||||
},
|
||||
categories: [],
|
||||
}
|
||||
|
||||
if (categories && Array.isArray(categories) && categories.length > 0) {
|
||||
// get full categories and keep a flattened copy of their most important properties
|
||||
try {
|
||||
const mappedCategories = categories.map((category) => {
|
||||
const { id, title } = category
|
||||
|
||||
return {
|
||||
relationTo: 'categories',
|
||||
id,
|
||||
title,
|
||||
}
|
||||
})
|
||||
|
||||
modifiedDoc.categories = mappedCategories
|
||||
} catch (_err) {
|
||||
console.error(
|
||||
`Failed. Category not found when syncing collection '${collection}' with id: '${id}' to search.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return modifiedDoc
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Field } from 'payload'
|
||||
|
||||
export const searchFields: Field[] = [
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'text',
|
||||
index: true,
|
||||
admin: {
|
||||
readOnly: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'meta',
|
||||
label: 'Meta',
|
||||
type: 'group',
|
||||
index: true,
|
||||
admin: {
|
||||
readOnly: true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'title',
|
||||
label: 'Title',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
},
|
||||
{
|
||||
name: 'image',
|
||||
label: 'Image',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Categories',
|
||||
name: 'categories',
|
||||
type: 'array',
|
||||
admin: {
|
||||
readOnly: true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'relationTo',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Post } from '@/payload-types'
|
||||
|
||||
/**
|
||||
* Formats an array of populatedAuthors from Posts into a prettified string.
|
||||
* @param authors - The populatedAuthors array from a Post.
|
||||
* @returns A prettified string of authors.
|
||||
* @example
|
||||
*
|
||||
* [Author1, Author2] becomes 'Author1 and Author2'
|
||||
* [Author1, Author2, Author3] becomes 'Author1, Author2, and Author3'
|
||||
*
|
||||
*/
|
||||
export const formatAuthors = (
|
||||
authors: NonNullable<NonNullable<Post['populatedAuthors']>[number]>[]
|
||||
) => {
|
||||
// Ensure we don't have any authors without a name
|
||||
const authorNames = authors.map((author) => author.name).filter(Boolean)
|
||||
|
||||
if (authorNames.length === 0) return ''
|
||||
if (authorNames.length === 1) return authorNames[0]
|
||||
if (authorNames.length === 2) return `${authorNames[0]} and ${authorNames[1]}`
|
||||
|
||||
return `${authorNames.slice(0, -1).join(', ')} and ${authorNames[authorNames.length - 1]}`
|
||||
}
|
||||
@@ -2,8 +2,8 @@ import type { Metadata } from 'next'
|
||||
|
||||
import type { Media, Post, Config } from '../payload-types'
|
||||
|
||||
import { mergeOpenGraph } from './mergeOpenGraph'
|
||||
import { getServerSideURL } from './getURL'
|
||||
import { mergeOpenGraph } from './mergeOpenGraph.ts'
|
||||
import { getServerSideURL } from './getURL.ts'
|
||||
|
||||
const getImageURL = (image?: Media | Config['db']['defaultIDType'] | null) => {
|
||||
const serverUrl = getServerSideURL()
|
||||
|
||||
@@ -2,7 +2,6 @@ import { PayloadRequest, CollectionSlug } from 'payload'
|
||||
|
||||
const collectionPrefixMap: Partial<Record<CollectionSlug, string>> = {
|
||||
posts: '/blog',
|
||||
pages: '',
|
||||
}
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Config } from 'src/payload-types'
|
||||
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
import { unstable_cache } from 'next/cache'
|
||||
// Do not import next/cache at module scope to keep migrate step Node-compatible
|
||||
|
||||
type Collection = keyof Config['collections']
|
||||
|
||||
@@ -25,7 +25,16 @@ async function getDocument(collection: Collection, slug: string, depth = 0) {
|
||||
/**
|
||||
* Returns a unstable_cache function mapped with the cache tag for the slug
|
||||
*/
|
||||
export const getCachedDocument = (collection: Collection, slug: string) =>
|
||||
unstable_cache(async () => getDocument(collection, slug), [collection, slug], {
|
||||
tags: [`${collection}_${slug}`],
|
||||
})
|
||||
export const getCachedDocument = (collection: Collection, slug: string) => {
|
||||
return async () => {
|
||||
try {
|
||||
const { unstable_cache } = await import('next/cache')
|
||||
const cached = unstable_cache(async () => getDocument(collection, slug), [collection, slug], {
|
||||
tags: [`${collection}_${slug}`],
|
||||
})
|
||||
return cached()
|
||||
} catch {
|
||||
return getDocument(collection, slug)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import type { Config } from 'src/payload-types'
|
||||
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
import { unstable_cache } from 'next/cache'
|
||||
// Do not import next/cache at module scope to keep migrate step Node-compatible
|
||||
|
||||
type Global = keyof Config['globals']
|
||||
|
||||
@@ -20,7 +20,16 @@ async function getGlobal(slug: Global, depth = 0) {
|
||||
/**
|
||||
* Returns a unstable_cache function mapped with the cache tag for the slug
|
||||
*/
|
||||
export const getCachedGlobal = (slug: Global, depth = 0) =>
|
||||
unstable_cache(async () => getGlobal(slug, depth), [slug], {
|
||||
tags: [`global_${slug}`],
|
||||
})
|
||||
export const getCachedGlobal = (slug: Global, depth = 0) => {
|
||||
return async () => {
|
||||
try {
|
||||
const { unstable_cache } = await import('next/cache')
|
||||
const cached = unstable_cache(async () => getGlobal(slug, depth), [slug], {
|
||||
tags: [`global_${slug}`],
|
||||
})
|
||||
return cached()
|
||||
} catch {
|
||||
return getGlobal(slug, depth)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { cookies } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
import type { User } from '../payload-types'
|
||||
import { getClientSideURL } from './getURL'
|
||||
import { getClientSideURL } from './getURL.ts'
|
||||
|
||||
export const getMeUser = async (args?: {
|
||||
nullUserRedirect?: string
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
import { unstable_cache } from 'next/cache'
|
||||
|
||||
export async function getRedirects(depth = 1) {
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
const { docs: redirects } = await payload.find({
|
||||
collection: 'redirects',
|
||||
depth,
|
||||
limit: 0,
|
||||
pagination: false,
|
||||
})
|
||||
|
||||
return redirects
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a unstable_cache function mapped with the cache tag for 'redirects'.
|
||||
*
|
||||
* Cache all redirects together to avoid multiple fetches.
|
||||
*/
|
||||
export const getCachedRedirects = () =>
|
||||
unstable_cache(async () => getRedirects(), ['redirects'], {
|
||||
tags: ['redirects'],
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import canUseDOM from './canUseDOM'
|
||||
import canUseDOM from './canUseDOM.ts'
|
||||
|
||||
export const getServerSideURL = () => {
|
||||
let url = process.env.NEXT_PUBLIC_SERVER_URL
|
||||
@@ -8,7 +8,7 @@ export const getServerSideURL = () => {
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
url = 'http://localhost:3000'
|
||||
url = 'http://localhost:3030'
|
||||
}
|
||||
|
||||
return url
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { getServerSideURL } from './getURL'
|
||||
import { getServerSideURL } from './getURL.ts'
|
||||
|
||||
const defaultOpenGraph: Metadata['openGraph'] = {
|
||||
type: 'website',
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
# https://supabase.com/docs/guides/local-development/cli/config
|
||||
# A string used to distinguish different Supabase projects on the same host. Defaults to the
|
||||
# working directory name when running `supabase init`.
|
||||
project_id = "cms-payload"
|
||||
project_id = "cms"
|
||||
|
||||
[api]
|
||||
enabled = true
|
||||
# Port to use for the API URL.
|
||||
port = 54321
|
||||
port = 34321
|
||||
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
|
||||
# endpoints. `public` and `graphql_public` schemas are included by default.
|
||||
schemas = ["public", "graphql_public"]
|
||||
@@ -23,17 +23,17 @@ enabled = false
|
||||
|
||||
[db]
|
||||
# Port to use for the local database URL.
|
||||
port = 54322
|
||||
port = 34322
|
||||
# Port used by db diff command to initialize the shadow database.
|
||||
shadow_port = 54320
|
||||
shadow_port = 34320
|
||||
# The database major version to use. This has to be the same as your remote database's. Run `SHOW
|
||||
# server_version;` on the remote database to check.
|
||||
major_version = 15
|
||||
major_version = 17
|
||||
|
||||
[db.pooler]
|
||||
enabled = false
|
||||
# Port to use for the local connection pooler.
|
||||
port = 54329
|
||||
port = 34329
|
||||
# Specifies when a server connection can be reused by other clients.
|
||||
# Configure one of the supported pooler modes: `transaction`, `session`.
|
||||
pool_mode = "transaction"
|
||||
@@ -46,6 +46,8 @@ max_client_conn = 100
|
||||
# secret_key = "env(SECRET_VALUE)"
|
||||
|
||||
[db.migrations]
|
||||
# If disabled, migrations will be skipped during a db push or reset.
|
||||
enabled = true
|
||||
# Specifies an ordered list of schema files that describe your database.
|
||||
# Supports glob patterns relative to supabase directory: "./schemas/*.sql"
|
||||
schema_paths = []
|
||||
@@ -57,6 +59,16 @@ enabled = true
|
||||
# Supports glob patterns relative to supabase directory: "./seeds/*.sql"
|
||||
sql_paths = ["./seed.sql"]
|
||||
|
||||
[db.network_restrictions]
|
||||
# Enable management of network restrictions.
|
||||
enabled = false
|
||||
# List of IPv4 CIDR blocks allowed to connect to the database.
|
||||
# Defaults to allow all IPv4 connections. Set empty array to block all IPs.
|
||||
allowed_cidrs = ["0.0.0.0/0"]
|
||||
# List of IPv6 CIDR blocks allowed to connect to the database.
|
||||
# Defaults to allow all IPv6 connections. Set empty array to block all IPs.
|
||||
allowed_cidrs_v6 = ["::/0"]
|
||||
|
||||
[realtime]
|
||||
enabled = true
|
||||
# Bind realtime via either IPv4 or IPv6. (default: IPv4)
|
||||
@@ -67,7 +79,7 @@ enabled = true
|
||||
[studio]
|
||||
enabled = true
|
||||
# Port to use for Supabase Studio.
|
||||
port = 54323
|
||||
port = 34323
|
||||
# External URL of the API server that frontend connects to.
|
||||
api_url = "http://127.0.0.1"
|
||||
# OpenAI API Key to use for Supabase AI in the Supabase Studio.
|
||||
@@ -78,10 +90,10 @@ openai_api_key = "env(OPENAI_API_KEY)"
|
||||
[inbucket]
|
||||
enabled = true
|
||||
# Port to use for the email testing server web interface.
|
||||
port = 54324
|
||||
port = 34324
|
||||
# Uncomment to expose additional ports for testing user applications that send emails.
|
||||
# smtp_port = 54325
|
||||
# pop3_port = 54326
|
||||
# smtp_port = 34325
|
||||
# pop3_port = 34326
|
||||
# admin_email = "admin@email.com"
|
||||
# sender_name = "Admin"
|
||||
|
||||
@@ -110,6 +122,8 @@ site_url = "http://127.0.0.1:3000"
|
||||
additional_redirect_urls = ["https://127.0.0.1:3000"]
|
||||
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
|
||||
jwt_expiry = 3600
|
||||
# Path to JWT signing key. DO NOT commit your signing keys file to git.
|
||||
# signing_keys_path = "./signing_keys.json"
|
||||
# If disabled, the refresh token will never expire.
|
||||
enable_refresh_token_rotation = true
|
||||
# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
|
||||
@@ -140,6 +154,8 @@ token_refresh = 150
|
||||
sign_in_sign_ups = 30
|
||||
# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address.
|
||||
token_verifications = 30
|
||||
# Number of Web3 logins that can be made in a 5 minute interval per IP address.
|
||||
web3 = 30
|
||||
|
||||
# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`.
|
||||
# [auth.captcha]
|
||||
@@ -200,6 +216,11 @@ max_frequency = "5s"
|
||||
# Force log out if the user has been inactive longer than the specified duration.
|
||||
# inactivity_timeout = "8h"
|
||||
|
||||
# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object.
|
||||
# [auth.hook.before_user_created]
|
||||
# enabled = true
|
||||
# uri = "pg-functions://postgres/auth/before-user-created-hook"
|
||||
|
||||
# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
|
||||
# [auth.hook.custom_access_token]
|
||||
# enabled = true
|
||||
@@ -252,6 +273,11 @@ url = ""
|
||||
# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
|
||||
skip_nonce_check = false
|
||||
|
||||
# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard.
|
||||
# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting.
|
||||
[auth.web3.solana]
|
||||
enabled = false
|
||||
|
||||
# Use Firebase Auth as a third-party provider alongside Supabase Auth.
|
||||
[auth.third_party.firebase]
|
||||
enabled = false
|
||||
@@ -276,7 +302,7 @@ enabled = false
|
||||
# domain = "example.clerk.accounts.dev"
|
||||
|
||||
[edge_runtime]
|
||||
enabled = true
|
||||
enabled = false
|
||||
# Configure one of the supported request policies: `oneshot`, `per_worker`.
|
||||
# Use `oneshot` for hot reload, or `per_worker` for load testing.
|
||||
policy = "oneshot"
|
||||
@@ -289,8 +315,8 @@ deno_version = 1
|
||||
# secret_key = "env(SECRET_VALUE)"
|
||||
|
||||
[analytics]
|
||||
enabled = true
|
||||
port = 54327
|
||||
enabled = false
|
||||
port = 34327
|
||||
# Configure one of the supported backends: `postgres`, `bigquery`.
|
||||
backend = "postgres"
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Strictness */
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
"baseUrl": ".",
|
||||
"esModuleInterop": true,
|
||||
"target": "ES2022",
|
||||
@@ -18,6 +16,7 @@
|
||||
"jsx": "preserve",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"sourceMap": true,
|
||||
"isolatedModules": true,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||
"buildCommand": "pnpm vercel-build",
|
||||
"installCommand": "pnpm install"
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -425,7 +425,7 @@ $$;
|
||||
create policy "rls_test_select"
|
||||
on test_table
|
||||
to authenticated
|
||||
using ( private.has_good_role() );
|
||||
using ( (select private.has_good_role()) );
|
||||
```
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
@@ -34,6 +34,19 @@ supabase --experimental branches create --persistent
|
||||
# Do you want to create a branch named develop? [Y/n]
|
||||
```
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
To retrieve the project ID for an existing branch, use the `branches list` command:
|
||||
|
||||
```bash
|
||||
supabase --experimental branches list
|
||||
```
|
||||
|
||||
This will display a table showing all your branches with their corresponding project ID.
|
||||
Use the value from the `BRANCH PROJECT ID` column as your `project_id` in the remote configuration.
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Configuration merging
|
||||
|
||||
When merging a PR into a persistent branch, the Supabase integration:
|
||||
@@ -186,6 +199,19 @@ max_rows = 500
|
||||
pool_size = 25
|
||||
```
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
To retrieve the project ID for an existing branch, use the `branches list` command:
|
||||
|
||||
```bash
|
||||
supabase --experimental branches list
|
||||
```
|
||||
|
||||
This will display a table showing all your branches with their corresponding project ID.
|
||||
Use the value from the `BRANCH PROJECT ID` column as your `project_id` in the remote configuration.
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Feature branch configuration
|
||||
|
||||
For feature branches that need specific settings:
|
||||
@@ -200,6 +226,19 @@ client_id = "env(GOOGLE_CLIENT_ID)"
|
||||
secret = "env(GOOGLE_CLIENT_SECRET)"
|
||||
```
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
To retrieve the project ID for an existing branch, use the `branches list` command:
|
||||
|
||||
```bash
|
||||
supabase --experimental branches list
|
||||
```
|
||||
|
||||
This will display a table showing all your branches with their corresponding project ID.
|
||||
Use the value from the `BRANCH PROJECT ID` column as your `project_id` in the remote configuration.
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Next steps
|
||||
|
||||
- Explore [branching integrations](/docs/guides/deployment/branching/integrations)
|
||||
|
||||
@@ -58,8 +58,6 @@ When reviewing a merge request you may see a notice at the top of the page askin
|
||||
There are a few limitations you should be aware of before deciding to use branching without git.
|
||||
|
||||
- Custom roles created through the dashboard are not captured on branch creation
|
||||
- Only public schema changes are supported right now
|
||||
- Extensions are not included in the diff process
|
||||
- Branches can only be merged to main; merging between preview branches is not supported
|
||||
- If your branch is out of date, you can pull in latest changes from main, but keep in mind that all functions will be overwritten
|
||||
- Deleting functions must be done manually on main branch
|
||||
|
||||
@@ -32,7 +32,7 @@ Here are some basic examples of setting up WebSocket servers using Deno and Node
|
||||
Deno.serve((req) => {
|
||||
const upgrade = req.headers.get('upgrade') || ''
|
||||
|
||||
if (upgrade.toLowerCase() != 'WebSocket') {
|
||||
if (upgrade.toLowerCase() != 'websocket') {
|
||||
return new Response("request isn't trying to upgrade to WebSocket.", { status: 400 })
|
||||
}
|
||||
|
||||
|
||||
@@ -59,12 +59,12 @@ hideToc: true
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={4}>
|
||||
<StepHikeCompact.Details title="Query Supabase data from Next.js">
|
||||
<StepHikeCompact.Step step={4}>
|
||||
<StepHikeCompact.Details title="Create Supabase client">
|
||||
|
||||
Create a new file at `app/instruments/page.tsx` and populate with the following.
|
||||
Create a new file at `utils/supabase/server.ts` and populate with the following.
|
||||
|
||||
This will select all the rows from the `instruments` table in Supabase and render them on the page.
|
||||
This creates a Supabase client, using the credentials from the `env.local` file.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
@@ -72,17 +72,6 @@ hideToc: true
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=app/instruments/page.tsx
|
||||
import { createClient } from '@/utils/supabase/server';
|
||||
|
||||
export default async function Instruments() {
|
||||
const supabase = await createClient();
|
||||
const { data: instruments } = await supabase.from("instruments").select();
|
||||
|
||||
return <pre>{JSON.stringify(instruments, null, 2)}</pre>
|
||||
}
|
||||
```
|
||||
|
||||
```ts name=utils/supabase/server.ts
|
||||
import { createServerClient } from '@supabase/ssr'
|
||||
import { cookies } from 'next/headers'
|
||||
@@ -122,6 +111,36 @@ hideToc: true
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={5}>
|
||||
<StepHikeCompact.Details title="Query Supabase data from Next.js">
|
||||
|
||||
Create a new file at `app/instruments/page.tsx` and populate with the following.
|
||||
|
||||
This selects all the rows from the `instruments` table in Supabase and render them on the page.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=app/instruments/page.tsx
|
||||
import { createClient } from '@/utils/supabase/server';
|
||||
|
||||
export default async function Instruments() {
|
||||
const supabase = await createClient();
|
||||
const { data: instruments } = await supabase.from("instruments").select();
|
||||
|
||||
return <pre>{JSON.stringify(instruments, null, 2)}</pre>
|
||||
}
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={6}>
|
||||
<StepHikeCompact.Details title="Start the app">
|
||||
|
||||
Run the development server, go to http://localhost:3000/instruments in a browser and you should see the list of instruments.
|
||||
|
||||
@@ -66,8 +66,8 @@ hideToc: true
|
||||
<$CodeTabs>
|
||||
|
||||
```text name=.env
|
||||
VITE_PUBLIC_SUPABASE_URL=<SUBSTITUTE_SUPABASE_URL>
|
||||
VITE_PUBLIC_SUPABASE_ANON_KEY=<SUBSTITUTE_SUPABASE_ANON_KEY>
|
||||
PUBLIC_SUPABASE_URL=<SUBSTITUTE_SUPABASE_URL>
|
||||
PUBLIC_SUPABASE_ANON_KEY=<SUBSTITUTE_SUPABASE_ANON_KEY>
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
@@ -89,16 +89,16 @@ hideToc: true
|
||||
|
||||
```js name=src/lib/supabaseClient.js
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { VITE_PUBLIC_SUPABASE_URL, VITE_PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
|
||||
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
|
||||
|
||||
export const supabase = createClient(VITE_PUBLIC_SUPABASE_URL, VITE_PUBLIC_SUPABASE_ANON_KEY)
|
||||
export const supabase = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY)
|
||||
```
|
||||
|
||||
```ts name=src/lib/supabaseClient.ts
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { VITE_PUBLIC_SUPABASE_URL, VITE_PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
|
||||
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
|
||||
|
||||
export const supabase = createClient(VITE_PUBLIC_SUPABASE_URL, VITE_PUBLIC_SUPABASE_ANON_KEY)
|
||||
export const supabase = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY)
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
@@ -17,14 +17,14 @@ If you get stuck while working through this guide, refer to the [full example on
|
||||
|
||||
## Building the app
|
||||
|
||||
Let's start building the Svelte app from scratch.
|
||||
Start building the Svelte app from scratch.
|
||||
|
||||
### Initialize a Svelte app
|
||||
|
||||
We can use the [SvelteKit Skeleton Project](https://kit.svelte.dev/docs) to initialize an app called `supabase-sveltekit` (for this tutorial we will be using TypeScript):
|
||||
Use the [SvelteKit Skeleton Project](https://svelte.dev/docs/kit) to initialize an app called `supabase-sveltekit` (for this tutorial, select "SvelteKit minimal" and use TypeScript):
|
||||
|
||||
```bash
|
||||
npm create svelte@latest supabase-sveltekit
|
||||
npx sv create supabase-sveltekit
|
||||
cd supabase-sveltekit
|
||||
npm install
|
||||
```
|
||||
@@ -35,8 +35,8 @@ Then install the Supabase client library: [supabase-js](https://github.com/supab
|
||||
npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
And finally we want to save the environment variables in a `.env`.
|
||||
All we need are the `SUPABASE_URL` and the `SUPABASE_KEY` key that you copied [earlier](#get-the-api-keys).
|
||||
And finally, save the environment variables in a `.env` file.
|
||||
All you need are the `PUBLIC_SUPABASE_URL` and the `PUBLIC_SUPABASE_ANON_KEY` key that you copied [earlier](#get-the-api-keys).
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
@@ -47,108 +47,46 @@ PUBLIC_SUPABASE_ANON_KEY="YOUR_SUPABASE_KEY"
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
Optionally, add `src/styles.css` with the [CSS from the example](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/sveltekit-user-management/src/styles.css).
|
||||
### App styling (optional)
|
||||
|
||||
An optional step is to update the CSS file `src/styles.css` to make the app look nice.
|
||||
You can find the full contents of this file [in the example repository](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/sveltekit-user-management/src/styles.css).
|
||||
|
||||
### Creating a Supabase client for SSR
|
||||
|
||||
The `ssr` package configures Supabase to use Cookies, which is required for server-side languages and frameworks.
|
||||
The `ssr` package configures Supabase to use Cookies, which are required for server-side languages and frameworks.
|
||||
|
||||
Install the Supabase packages:
|
||||
Install the SSR package:
|
||||
|
||||
```bash
|
||||
npm install @supabase/ssr @supabase/supabase-js
|
||||
npm install @supabase/ssr
|
||||
```
|
||||
|
||||
Creating a Supabase client with the `ssr` package automatically configures it to use Cookies. This means your user's session is available throughout the entire SvelteKit stack - page, layout, server, hooks.
|
||||
Creating a Supabase client with the `ssr` package automatically configures it to use Cookies. This means the user's session is available throughout the entire SvelteKit stack - page, layout, server, and hooks.
|
||||
|
||||
Add the code below to your `src/hooks.server.ts` to initialize the client on the server:
|
||||
Add the code below to a `src/hooks.server.ts` file to initialize the client on the server:
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/hooks.server.ts
|
||||
// src/hooks.server.ts
|
||||
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'
|
||||
import { createServerClient } from '@supabase/ssr'
|
||||
import type { Handle } from '@sveltejs/kit'
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||
cookies: {
|
||||
getAll: () => event.cookies.getAll(),
|
||||
/**
|
||||
* SvelteKit's cookies API requires `path` to be explicitly set in
|
||||
* the cookie options. Setting `path` to `/` replicates previous/
|
||||
* standard behavior.
|
||||
*/
|
||||
setAll: (cookiesToSet) => {
|
||||
cookiesToSet.forEach(({ name, value, options }) => {
|
||||
event.cookies.set(name, value, { ...options, path: '/' })
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Unlike `supabase.auth.getSession()`, which returns the session _without_
|
||||
* validating the JWT, this function also calls `getUser()` to validate the
|
||||
* JWT before returning the session.
|
||||
*/
|
||||
event.locals.safeGetSession = async () => {
|
||||
const {
|
||||
data: { session },
|
||||
} = await event.locals.supabase.auth.getSession()
|
||||
if (!session) {
|
||||
return { session: null, user: null }
|
||||
}
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await event.locals.supabase.auth.getUser()
|
||||
if (error) {
|
||||
// JWT validation has failed
|
||||
return { session: null, user: null }
|
||||
}
|
||||
|
||||
return { session, user }
|
||||
}
|
||||
|
||||
return resolve(event, {
|
||||
filterSerializedResponseHeaders(name) {
|
||||
return name === 'content-range' || name === 'x-supabase-api-version'
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/sveltekit-user-management/src/hooks.server.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/hooks.server.ts"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
<$Partial path="get_session_warning.mdx" />
|
||||
|
||||
If you are using TypeScript the compiler might complain about `event.locals.supabase` and `event.locals.safeGetSession`, this can be fixed by updating your `src/app.d.ts` with the content below:
|
||||
{/* TODO: Change when adding JS autoconversion */}
|
||||
As this tutorial uses TypeScript the compiler complains about `event.locals.supabase` and `event.locals.safeGetSession`, you can fix this by updating the `src/app.d.ts` with the content below:
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/app.d.ts
|
||||
// src/app.d.ts
|
||||
|
||||
import { SupabaseClient, Session } from '@supabase/supabase-js'
|
||||
|
||||
declare global {
|
||||
namespace App {
|
||||
interface Locals {
|
||||
supabase: SupabaseClient
|
||||
safeGetSession(): Promise<{ session: Session | null; user: User | null }>
|
||||
}
|
||||
interface PageData {
|
||||
session: Session | null
|
||||
user: User | null
|
||||
}
|
||||
// interface Error {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/sveltekit-user-management/src/app.d.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/app.d.ts"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
@@ -156,26 +94,17 @@ Create a new `src/routes/+layout.server.ts` file to handle the session on the se
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/routes/+layout.server.ts
|
||||
// src/routes/+layout.server.ts
|
||||
import type { LayoutServerLoad } from './$types'
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals: { safeGetSession }, cookies }) => {
|
||||
const { session, user } = await safeGetSession()
|
||||
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
cookies: cookies.getAll(),
|
||||
}
|
||||
}
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/sveltekit-user-management/src/routes/+layout.server.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/routes/+layout.server.ts"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
Start your dev server (`npm run dev`) in order to generate the `./$types` files we are referencing in our project.
|
||||
Start the dev server (`npm run dev`) to generate the `./$types` files we are referencing in our project.
|
||||
|
||||
</Admonition>
|
||||
|
||||
@@ -183,202 +112,49 @@ Create a new `src/routes/+layout.ts` file to handle the session and the `supabas
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/routes/+layout.ts
|
||||
// src/routes/+layout.ts
|
||||
import { createBrowserClient, createServerClient, isBrowser } from '@supabase/ssr'
|
||||
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public'
|
||||
import type { LayoutLoad } from './$types'
|
||||
|
||||
export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
||||
depends('supabase:auth')
|
||||
|
||||
const supabase = isBrowser()
|
||||
? createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||
global: {
|
||||
fetch,
|
||||
},
|
||||
})
|
||||
: createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||
global: {
|
||||
fetch,
|
||||
},
|
||||
cookies: {
|
||||
getAll() {
|
||||
return data.cookies
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* It's fine to use `getSession` here, because on the client, `getSession` is
|
||||
* safe, and on the server, it reads `session` from the `LayoutData`, which
|
||||
* safely checked the session using `safeGetSession`.
|
||||
*/
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
|
||||
return { supabase, session }
|
||||
}
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/sveltekit-user-management/src/routes/+layout.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/routes/+layout.ts"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
Update your `src/routes/+layout.svelte`:
|
||||
Create `src/routes/+layout.svelte`:
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```svelte name=src/routes/+layout.svelte
|
||||
<!-- src/routes/+layout.svelte -->
|
||||
<script lang="ts">
|
||||
import '../styles.css'
|
||||
import { invalidate } from '$app/navigation'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
export let data
|
||||
|
||||
let { supabase, session } = data
|
||||
$: ({ supabase, session } = data)
|
||||
|
||||
onMount(() => {
|
||||
const { data } = supabase.auth.onAuthStateChange((event, newSession) => {
|
||||
if (newSession?.expires_at !== session?.expires_at) {
|
||||
invalidate('supabase:auth')
|
||||
}
|
||||
})
|
||||
|
||||
return () => data.subscription.unsubscribe()
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>User Management</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container" style="padding: 50px 0 100px 0">
|
||||
<slot />
|
||||
</div>
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/sveltekit-user-management/src/routes/+layout.svelte"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/routes/+layout.svelte"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
## Set up a login page
|
||||
|
||||
Create a magic link login/signup page for your application:
|
||||
Create a magic link login/signup page for your application by updating the `routes/+page.svelte` file:
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```svelte name=src/routes/+page.svelte
|
||||
<!-- src/routes/+page.svelte -->
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms'
|
||||
import type { ActionData, SubmitFunction } from './$types.js'
|
||||
|
||||
export let form: ActionData;
|
||||
|
||||
let loading = false
|
||||
|
||||
const handleSubmit: SubmitFunction = () => {
|
||||
loading = true
|
||||
return async ({ update }) => {
|
||||
update()
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>User Management</title>
|
||||
</svelte:head>
|
||||
|
||||
<form class="row flex flex-center" method="POST" use:enhance={handleSubmit}>
|
||||
<div class="col-6 form-widget">
|
||||
<h1 class="header">Supabase + SvelteKit</h1>
|
||||
<p class="description">Sign in via magic link with your email below</p>
|
||||
{#if form?.message !== undefined}
|
||||
<div class="success {form?.success ? '' : 'fail'}">
|
||||
{form?.message}
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<label for="email">Email address</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
class="inputField"
|
||||
type="email"
|
||||
placeholder="Your email"
|
||||
value={form?.email ?? ''}
|
||||
/>
|
||||
</div>
|
||||
{#if form?.errors?.email}
|
||||
<span class="flex items-center text-sm error">
|
||||
{form?.errors?.email}
|
||||
</span>
|
||||
{/if}
|
||||
<div>
|
||||
<button class="button primary block">
|
||||
{ loading ? 'Loading' : 'Send magic link' }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/sveltekit-user-management/src/routes/+page.svelte"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/routes/+page.svelte"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
Create a `src/routes/+page.server.ts` file that will handle our magic link form when submitted.
|
||||
Create a `src/routes/+page.server.ts` file that handles the magic link form when submitted.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/routes/+page.server.ts
|
||||
// src/routes/+page.server.ts
|
||||
import { fail, redirect } from '@sveltejs/kit'
|
||||
import type { Actions, PageServerLoad } from './$types'
|
||||
|
||||
export const load: PageServerLoad = async ({ url, locals: { safeGetSession } }) => {
|
||||
const { session } = await safeGetSession()
|
||||
|
||||
// if the user is already logged in return them to the account page
|
||||
if (session) {
|
||||
redirect(303, '/account')
|
||||
}
|
||||
|
||||
return { url: url.origin }
|
||||
}
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async (event) => {
|
||||
const {
|
||||
url,
|
||||
request,
|
||||
locals: { supabase },
|
||||
} = event
|
||||
const formData = await request.formData()
|
||||
const email = formData.get('email') as string
|
||||
const validEmail = /^[\w-\.+]+@([\w-]+\.)+[\w-]{2,8}$/.test(email)
|
||||
|
||||
if (!validEmail) {
|
||||
return fail(400, { errors: { email: 'Please enter a valid email address' }, email })
|
||||
}
|
||||
|
||||
const { error } = await supabase.auth.signInWithOtp({ email })
|
||||
|
||||
if (error) {
|
||||
return fail(400, {
|
||||
success: false,
|
||||
email,
|
||||
message: `There was an issue, Please contact support.`,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Please check your email for a magic link to log into the website.',
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/sveltekit-user-management/src/routes/+page.server.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/routes/+page.server.ts"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
@@ -388,239 +164,77 @@ Change the email template to support a server-side authentication flow.
|
||||
|
||||
Before we proceed, let's change the email template to support sending a token hash:
|
||||
|
||||
- Go to the [Auth templates](/dashboard/project/_/auth/templates) page in your dashboard.
|
||||
- Select `Confirm signup` template.
|
||||
- Go to the [**Auth** > **Emails**](/dashboard/project/_/auth/templates) page in the project dashboard.
|
||||
- Select the **Confirm signup** template.
|
||||
- Change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email`.
|
||||
- Repeat the previous step for `Magic link` template.
|
||||
- Repeat the previous step for **Magic link** template.
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
Did you know? You could also customize emails sent out to new users, including the email's looks, content, and query parameters. Check out the [settings of your project](/dashboard/project/_/auth/templates).
|
||||
**Did you know?** You can also customize emails sent out to new users, including the email's looks, content, and query parameters. Check out the [settings of your project](/dashboard/project/_/auth/templates).
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Confirmation endpoint
|
||||
|
||||
As we are working in a server-side rendering (SSR) environment, it is necessary to create a server endpoint responsible for exchanging the `token_hash` for a session.
|
||||
As this is a server-side rendering (SSR) environment, you need to create a server endpoint responsible for exchanging the `token_hash` for a session.
|
||||
|
||||
In the following code snippet, we perform the following steps:
|
||||
The following code snippet performs the following steps:
|
||||
|
||||
- Retrieve the `token_hash` sent back from the Supabase Auth server using the `token_hash` query parameter.
|
||||
- Exchange this `token_hash` for a session, which we store in storage (in this case, cookies).
|
||||
- Finally, the user is redirected to the `account` page or the `error` page.
|
||||
- Retrieves the `token_hash` sent back from the Supabase Auth server using the `token_hash` query parameter.
|
||||
- Exchanges this `token_hash` for a session, which you store in storage (in this case, cookies).
|
||||
- Finally, redirect the user to the `account` page or the `error` page.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/routes/auth/confirm/+server.ts
|
||||
// src/routes/auth/confirm/+server.ts
|
||||
import type { EmailOtpType } from '@supabase/supabase-js'
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
|
||||
import type { RequestHandler } from './$types'
|
||||
|
||||
export const GET: RequestHandler = async ({ url, locals: { supabase } }) => {
|
||||
const token_hash = url.searchParams.get('token_hash')
|
||||
const type = url.searchParams.get('type') as EmailOtpType | null
|
||||
const next = url.searchParams.get('next') ?? '/account'
|
||||
|
||||
/**
|
||||
* Clean up the redirect URL by deleting the Auth flow parameters.
|
||||
*
|
||||
* `next` is preserved for now, because it's needed in the error case.
|
||||
*/
|
||||
const redirectTo = new URL(url)
|
||||
redirectTo.pathname = next
|
||||
redirectTo.searchParams.delete('token_hash')
|
||||
redirectTo.searchParams.delete('type')
|
||||
|
||||
if (token_hash && type) {
|
||||
const { error } = await supabase.auth.verifyOtp({ type, token_hash })
|
||||
if (!error) {
|
||||
redirectTo.searchParams.delete('next')
|
||||
redirect(303, redirectTo)
|
||||
}
|
||||
}
|
||||
|
||||
redirectTo.pathname = '/auth/error'
|
||||
redirect(303, redirectTo)
|
||||
}
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/sveltekit-user-management/src/routes/auth/confirm/+server.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/routes/auth/confirm/+server.ts"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
### Authentication error page
|
||||
|
||||
If there is an error with confirming the token you will be redirect to this error page.
|
||||
If there is an error with confirming the token, redirect the user to an error page.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```svelte name=src/routes/auth/error/+page.svelte
|
||||
<p>Login error</p>
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/sveltekit-user-management/src/routes/auth/error/+page.svelte"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/routes/auth/error/+page.svelte"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
### Account page
|
||||
|
||||
After a user is signed in, they need to be able to edit their profile details and manage their account.
|
||||
After a user signs in, they need to be able to edit their profile details page.
|
||||
Create a new `src/routes/account/+page.svelte` file with the content below.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```svelte name=src/routes/account/+page.svelte
|
||||
<!-- src/routes/account/+page.svelte -->
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { SubmitFunction } from '@sveltejs/kit';
|
||||
|
||||
export let data
|
||||
export let form
|
||||
|
||||
let { session, supabase, profile } = data
|
||||
$: ({ session, supabase, profile } = data)
|
||||
|
||||
let profileForm: HTMLFormElement
|
||||
let loading = false
|
||||
let fullName: string = profile?.full_name ?? ''
|
||||
let username: string = profile?.username ?? ''
|
||||
let website: string = profile?.website ?? ''
|
||||
let avatarUrl: string = profile?.avatar_url ?? ''
|
||||
|
||||
const handleSubmit: SubmitFunction = () => {
|
||||
loading = true
|
||||
return async () => {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSignOut: SubmitFunction = () => {
|
||||
loading = true
|
||||
return async ({ update }) => {
|
||||
loading = false
|
||||
update()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="form-widget">
|
||||
<form
|
||||
class="form-widget"
|
||||
method="post"
|
||||
action="?/update"
|
||||
use:enhance={handleSubmit}
|
||||
bind:this={profileForm}
|
||||
>
|
||||
<div>
|
||||
<label for="email">Email</label>
|
||||
<input id="email" type="text" value={session.user.email} disabled />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="fullName">Full Name</label>
|
||||
<input id="fullName" name="fullName" type="text" value={form?.fullName ?? fullName} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="username">Username</label>
|
||||
<input id="username" name="username" type="text" value={form?.username ?? username} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="website">Website</label>
|
||||
<input id="website" name="website" type="url" value={form?.website ?? website} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
type="submit"
|
||||
class="button block primary"
|
||||
value={loading ? 'Loading...' : 'Update'}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="post" action="?/signout" use:enhance={handleSignOut}>
|
||||
<div>
|
||||
<button class="button block" disabled={loading}>Sign Out</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/sveltekit-user-management/src/routes/account/+page.svelte"
|
||||
lines={[[1, 3],[6,12],[15,38],[47,-1]]}
|
||||
meta="name=src/routes/account/+page.svelte"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
Now create the associated `src/routes/account/+page.server.ts` file that will handle loading our data from the server through the `load` function
|
||||
and handle all our form actions through the `actions` object.
|
||||
Now, create the associated `src/routes/account/+page.server.ts` file that handles loading data from the server through the `load` function
|
||||
and handle all form actions through the `actions` object.
|
||||
|
||||
```ts name=src/routes/account/+page.server.ts
|
||||
import { fail, redirect } from '@sveltejs/kit'
|
||||
import type { Actions, PageServerLoad } from './$types'
|
||||
|
||||
export const load: PageServerLoad = async ({ locals: { supabase, safeGetSession } }) => {
|
||||
const { session } = await safeGetSession()
|
||||
|
||||
if (!session) {
|
||||
redirect(303, '/')
|
||||
}
|
||||
|
||||
const { data: profile } = await supabase
|
||||
.from('profiles')
|
||||
.select(`username, full_name, website, avatar_url`)
|
||||
.eq('id', session.user.id)
|
||||
.single()
|
||||
|
||||
return { session, profile }
|
||||
}
|
||||
|
||||
export const actions: Actions = {
|
||||
update: async ({ request, locals: { supabase, safeGetSession } }) => {
|
||||
const formData = await request.formData()
|
||||
const fullName = formData.get('fullName') as string
|
||||
const username = formData.get('username') as string
|
||||
const website = formData.get('website') as string
|
||||
const avatarUrl = formData.get('avatarUrl') as string
|
||||
|
||||
const { session } = await safeGetSession()
|
||||
|
||||
const { error } = await supabase.from('profiles').upsert({
|
||||
id: session?.user.id,
|
||||
full_name: fullName,
|
||||
username,
|
||||
website,
|
||||
avatar_url: avatarUrl,
|
||||
updated_at: new Date(),
|
||||
})
|
||||
|
||||
if (error) {
|
||||
return fail(500, {
|
||||
fullName,
|
||||
username,
|
||||
website,
|
||||
avatarUrl,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
fullName,
|
||||
username,
|
||||
website,
|
||||
avatarUrl,
|
||||
}
|
||||
},
|
||||
signout: async ({ locals: { supabase, safeGetSession } }) => {
|
||||
const { session } = await safeGetSession()
|
||||
if (session) {
|
||||
await supabase.auth.signOut()
|
||||
redirect(303, '/')
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/sveltekit-user-management/src/routes/+page.server.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/routes/+page.server.ts"
|
||||
/>
|
||||
|
||||
### Launch!
|
||||
|
||||
Now that we have all the pages in place, run this in a terminal window:
|
||||
With all the pages in place, run this command in a terminal:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
@@ -636,144 +250,29 @@ Every Supabase project is configured with [Storage](/docs/guides/storage) for ma
|
||||
|
||||
### Create an upload widget
|
||||
|
||||
Let's create an avatar for the user so that they can upload a profile photo. We can start by creating a new component called `Avatar.svelte` in the `src/routes/account` directory:
|
||||
Create an avatar for the user so that they can upload a profile photo. Start by creating a new component called `Avatar.svelte` in the `src/routes/account` directory:
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```svelte name=src/routes/account/Avatar.svelte
|
||||
<!-- src/routes/account/Avatar.svelte -->
|
||||
<script lang="ts">
|
||||
import type { SupabaseClient } from '@supabase/supabase-js'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let size = 10
|
||||
export let url: string
|
||||
export let supabase: SupabaseClient
|
||||
|
||||
let avatarUrl: string | null = null
|
||||
let uploading = false
|
||||
let files: FileList
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const downloadImage = async (path: string) => {
|
||||
try {
|
||||
const { data, error } = await supabase.storage.from('avatars').download(path)
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
const url = URL.createObjectURL(data)
|
||||
avatarUrl = url
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.log('Error downloading image: ', error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const uploadAvatar = async () => {
|
||||
try {
|
||||
uploading = true
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
throw new Error('You must select an image to upload.')
|
||||
}
|
||||
|
||||
const file = files[0]
|
||||
const fileExt = file.name.split('.').pop()
|
||||
const filePath = `${Math.random()}.${fileExt}`
|
||||
|
||||
const { error } = await supabase.storage.from('avatars').upload(filePath, file)
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
url = filePath
|
||||
setTimeout(() => {
|
||||
dispatch('upload')
|
||||
}, 100)
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
uploading = false
|
||||
}
|
||||
}
|
||||
|
||||
$: if (url) downloadImage(url)
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if avatarUrl}
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt={avatarUrl ? 'Avatar' : 'No image'}
|
||||
class="avatar image"
|
||||
style="height: {size}em; width: {size}em;"
|
||||
/>
|
||||
{:else}
|
||||
<div class="avatar no-image" style="height: {size}em; width: {size}em;" />
|
||||
{/if}
|
||||
<input type="hidden" name="avatarUrl" value={url} />
|
||||
|
||||
<div style="width: {size}em;">
|
||||
<label class="button primary block" for="single">
|
||||
{uploading ? 'Uploading ...' : 'Upload'}
|
||||
</label>
|
||||
<input
|
||||
style="visibility: hidden; position:absolute;"
|
||||
type="file"
|
||||
id="single"
|
||||
accept="image/*"
|
||||
bind:files
|
||||
on:change={uploadAvatar}
|
||||
disabled={uploading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/sveltekit-user-management/src/routes/account/Avatar.svelte"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/routes/account/Avatar.svelte"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
### Add the new widget
|
||||
|
||||
And then we can add the widget to the Account page:
|
||||
Add the widget to the Account page:
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```svelte name=src/routes/account/+page.svelte
|
||||
<!-- src/routes/account/+page.svelte -->
|
||||
<script lang="ts">
|
||||
// Import the new component
|
||||
import Avatar from './Avatar.svelte'
|
||||
</script>
|
||||
|
||||
<div class="form-widget">
|
||||
<form
|
||||
class="form-widget"
|
||||
method="post"
|
||||
action="?/update"
|
||||
use:enhance={handleSubmit}
|
||||
bind:this={profileForm}
|
||||
>
|
||||
<!-- Add to body -->
|
||||
<Avatar
|
||||
{supabase}
|
||||
bind:url={avatarUrl}
|
||||
size={10}
|
||||
on:upload={() => {
|
||||
profileForm.requestSubmit();
|
||||
}}
|
||||
/>
|
||||
|
||||
<!-- Other form elements -->
|
||||
</form>
|
||||
</div>
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/sveltekit-user-management/src/routes/account/+page.svelte"
|
||||
lines={[[1, 1], [4,4], [31,31],[39,46],[82,82]]}
|
||||
meta="name=src/routes/account/+page.svelte"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
|
||||
Loaded 100 of 412 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user