feat: contact form
This commit is contained in:
parent
436a076657
commit
a66c8969b4
13
.env.example
13
.env.example
@ -1,7 +1,20 @@
|
|||||||
DATABASE_URI=postgres://postgres:<password>@127.0.0.1:5432/your-database-name
|
DATABASE_URI=postgres://postgres:<password>@127.0.0.1:5432/your-database-name
|
||||||
PAYLOAD_SECRET=YOUR_SECRET_HERE
|
PAYLOAD_SECRET=YOUR_SECRET_HERE
|
||||||
|
|
||||||
|
DOMAIN_NAME=localhost:3000
|
||||||
PORT=3000
|
PORT=3000
|
||||||
|
|
||||||
ACCESS_KEY_ID=
|
ACCESS_KEY_ID=
|
||||||
SECRET_ACCESS_KEY=
|
SECRET_ACCESS_KEY=
|
||||||
BUCKET_NAME=
|
BUCKET_NAME=
|
||||||
S3_ENDPOINT=
|
S3_ENDPOINT=
|
||||||
|
|
||||||
|
DEFAULT_EMAIL=
|
||||||
|
SMTP_HOST=
|
||||||
|
SMTP_USER=
|
||||||
|
SMTP_PASS=
|
||||||
|
SMTP_PORT=587
|
||||||
|
PASSWORD_RESET_EXPIRATION_IN_MINUTES=30
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
115
package-lock.json
generated
115
package-lock.json
generated
@ -12,10 +12,12 @@
|
|||||||
"@payloadcms/db-postgres": "3.33.0",
|
"@payloadcms/db-postgres": "3.33.0",
|
||||||
"@payloadcms/next": "3.33.0",
|
"@payloadcms/next": "3.33.0",
|
||||||
"@payloadcms/payload-cloud": "3.33.0",
|
"@payloadcms/payload-cloud": "3.33.0",
|
||||||
|
"@payloadcms/plugin-form-builder": "3.33.0",
|
||||||
"@payloadcms/richtext-lexical": "3.33.0",
|
"@payloadcms/richtext-lexical": "3.33.0",
|
||||||
"@payloadcms/storage-s3": "3.33.0",
|
"@payloadcms/storage-s3": "3.33.0",
|
||||||
"@radix-ui/react-avatar": "^1.1.7",
|
"@radix-ui/react-avatar": "^1.1.7",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
|
"@radix-ui/react-label": "^2.1.6",
|
||||||
"@radix-ui/react-separator": "^1.1.4",
|
"@radix-ui/react-separator": "^1.1.4",
|
||||||
"@radix-ui/react-tooltip": "^1.2.4",
|
"@radix-ui/react-tooltip": "^1.2.4",
|
||||||
"@tailwindcss/postcss": "^4.1.4",
|
"@tailwindcss/postcss": "^4.1.4",
|
||||||
@ -30,8 +32,10 @@
|
|||||||
"payload": "3.33.0",
|
"payload": "3.33.0",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
|
"react-hook-form": "^7.56.2",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"sharp": "0.32.6",
|
"sharp": "0.32.6",
|
||||||
|
"sonner": "^2.0.3",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.2.0",
|
||||||
"tw-animate-css": "^1.2.8"
|
"tw-animate-css": "^1.2.8"
|
||||||
},
|
},
|
||||||
@ -3707,6 +3711,21 @@
|
|||||||
"react-dom": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020"
|
"react-dom": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@payloadcms/plugin-form-builder": {
|
||||||
|
"version": "3.33.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@payloadcms/plugin-form-builder/-/plugin-form-builder-3.33.0.tgz",
|
||||||
|
"integrity": "sha512-9i5tG144VGtO7zfrIzT5AwYZlgiVjvJ8ud7Ok/iuaYSk6H2owf7vcDY0tAAp5sf6pGrv1brRHPej5hD2K2Posg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@payloadcms/ui": "3.33.0",
|
||||||
|
"escape-html": "^1.0.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"payload": "3.33.0",
|
||||||
|
"react": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020",
|
||||||
|
"react-dom": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@payloadcms/richtext-lexical": {
|
"node_modules/@payloadcms/richtext-lexical": {
|
||||||
"version": "3.33.0",
|
"version": "3.33.0",
|
||||||
"resolved": "https://registry.npmjs.org/@payloadcms/richtext-lexical/-/richtext-lexical-3.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/@payloadcms/richtext-lexical/-/richtext-lexical-3.33.0.tgz",
|
||||||
@ -3818,6 +3837,16 @@
|
|||||||
"react-dom": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020"
|
"react-dom": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@payloadcms/ui/node_modules/sonner": {
|
||||||
|
"version": "1.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz",
|
||||||
|
"integrity": "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/primitive": {
|
"node_modules/@radix-ui/primitive": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
|
||||||
@ -3958,6 +3987,70 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-label": {
|
||||||
|
"version": "2.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.6.tgz",
|
||||||
|
"integrity": "sha512-S/hv1mTlgcPX2gCTJrWuTjSXf7ER3Zf7zWGtOprxhIIY93Qin3n5VgNA0Ez9AgrK/lEtlYgzLd4f5x6AVar4Yw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.2.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-popper": {
|
"node_modules/@radix-ui/react-popper": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.4.tgz",
|
||||||
@ -12309,6 +12402,22 @@
|
|||||||
"react": ">=16.13.1"
|
"react": ">=16.13.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-hook-form": {
|
||||||
|
"version": "7.56.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.56.2.tgz",
|
||||||
|
"integrity": "sha512-vpfuHuQMF/L6GpuQ4c3ZDo+pRYxIi40gQqsCmmfUBwm+oqvBhKhwghCuj2o00YCgSfU6bR9KC/xnQGWm3Gr08A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/react-hook-form"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-image-crop": {
|
"node_modules/react-image-crop": {
|
||||||
"version": "10.1.8",
|
"version": "10.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/react-image-crop/-/react-image-crop-10.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/react-image-crop/-/react-image-crop-10.1.8.tgz",
|
||||||
@ -12992,9 +13101,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sonner": {
|
"node_modules/sonner": {
|
||||||
"version": "1.7.4",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.3.tgz",
|
||||||
"integrity": "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==",
|
"integrity": "sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
|
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
|
||||||
|
|||||||
@ -18,10 +18,12 @@
|
|||||||
"@payloadcms/db-postgres": "3.33.0",
|
"@payloadcms/db-postgres": "3.33.0",
|
||||||
"@payloadcms/next": "3.33.0",
|
"@payloadcms/next": "3.33.0",
|
||||||
"@payloadcms/payload-cloud": "3.33.0",
|
"@payloadcms/payload-cloud": "3.33.0",
|
||||||
|
"@payloadcms/plugin-form-builder": "3.33.0",
|
||||||
"@payloadcms/richtext-lexical": "3.33.0",
|
"@payloadcms/richtext-lexical": "3.33.0",
|
||||||
"@payloadcms/storage-s3": "3.33.0",
|
"@payloadcms/storage-s3": "3.33.0",
|
||||||
"@radix-ui/react-avatar": "^1.1.7",
|
"@radix-ui/react-avatar": "^1.1.7",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
|
"@radix-ui/react-label": "^2.1.6",
|
||||||
"@radix-ui/react-separator": "^1.1.4",
|
"@radix-ui/react-separator": "^1.1.4",
|
||||||
"@radix-ui/react-tooltip": "^1.2.4",
|
"@radix-ui/react-tooltip": "^1.2.4",
|
||||||
"@tailwindcss/postcss": "^4.1.4",
|
"@tailwindcss/postcss": "^4.1.4",
|
||||||
@ -36,8 +38,10 @@
|
|||||||
"payload": "3.33.0",
|
"payload": "3.33.0",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
|
"react-hook-form": "^7.56.2",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"sharp": "0.32.6",
|
"sharp": "0.32.6",
|
||||||
|
"sonner": "^2.0.3",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.2.0",
|
||||||
"tw-animate-css": "^1.2.8"
|
"tw-animate-css": "^1.2.8"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { Inter as FontSans } from 'next/font/google'
|
|||||||
import { getPayload } from 'payload'
|
import { getPayload } from 'payload'
|
||||||
import configPromise from '@payload-config'
|
import configPromise from '@payload-config'
|
||||||
import Navbar from '@/globals/Nav/component'
|
import Navbar from '@/globals/Nav/component'
|
||||||
|
import { Toaster } from '@/components/ui/sonner'
|
||||||
|
|
||||||
const fontSans = FontSans({
|
const fontSans = FontSans({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
@ -74,6 +75,7 @@ export default function RootLayout({
|
|||||||
<ThemeProvider attribute="class" defaultTheme="light">
|
<ThemeProvider attribute="class" defaultTheme="light">
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<main>{children}</main>
|
<main>{children}</main>
|
||||||
|
<Toaster />
|
||||||
<Navbar {...navProps} />
|
<Navbar {...navProps} />
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
146
src/blocks/ContactForm/component.tsx
Normal file
146
src/blocks/ContactForm/component.tsx
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { FormEvent, useState } from 'react'
|
||||||
|
import {
|
||||||
|
PopoverForm,
|
||||||
|
PopoverFormButton,
|
||||||
|
PopoverFormCutOutLeftIcon,
|
||||||
|
PopoverFormCutOutRightIcon,
|
||||||
|
PopoverFormSeparator,
|
||||||
|
PopoverFormSuccess,
|
||||||
|
} from '../../components/popover-form'
|
||||||
|
import type { Form as ContactFormData, ContactFormBlock as ContactFormProps } from '@/payload-types'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className?: string
|
||||||
|
} & ContactFormProps
|
||||||
|
|
||||||
|
type FormState = 'idle' | 'loading' | 'success'
|
||||||
|
|
||||||
|
export const ContactForm = (props: Props) => {
|
||||||
|
const [formState, setFormState] = useState<FormState>('idle')
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
const { form } = props
|
||||||
|
const { title, fields, submitButtonLabel, id } = form as ContactFormData
|
||||||
|
|
||||||
|
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (formState !== 'idle') return
|
||||||
|
setFormState('loading')
|
||||||
|
|
||||||
|
const formData = new FormData(e.currentTarget)
|
||||||
|
|
||||||
|
console.log('...', [...formData.entries()])
|
||||||
|
console.log('not spread', Object.entries(formData))
|
||||||
|
|
||||||
|
const dataToSend = [...formData.entries()].map(([name, value]) => {
|
||||||
|
return {
|
||||||
|
field: name,
|
||||||
|
value,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log('datatosend', dataToSend)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const submitResponse = await fetch('/api/form-submissions', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
form: id,
|
||||||
|
submissionData: dataToSend,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
console.log(submitResponse)
|
||||||
|
if (submitResponse.status !== 201) {
|
||||||
|
setFormState('idle')
|
||||||
|
toast('There was an issue with your submission. Please try again.')
|
||||||
|
} else setFormState('success')
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
setFormState('idle')
|
||||||
|
toast('There was an issue with your submission. Please try again.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderFields = (fields: Record<string, any>[]) => {
|
||||||
|
return fields?.map((f) => {
|
||||||
|
switch (f.blockType) {
|
||||||
|
case 'text':
|
||||||
|
return (
|
||||||
|
<div key={f.name} className={`grid w-[${f.width}] max-w-sm items-center gap-1.5`}>
|
||||||
|
<Label className="text-muted-foreground" htmlFor={f.name}>
|
||||||
|
{f.label}
|
||||||
|
</Label>
|
||||||
|
<Input type="text" id={f.name} name={f.name} required={f.required} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
case 'textarea':
|
||||||
|
return (
|
||||||
|
<div key={f.name} className={`grid w-[${f.width}] max-w-sm items-center gap-1.5`}>
|
||||||
|
<Label className="text-muted-foreground" htmlFor={f.name}>
|
||||||
|
{f.label}
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Type your message here."
|
||||||
|
id={f.name}
|
||||||
|
name={f.name}
|
||||||
|
required={f.required}
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
case 'email':
|
||||||
|
return (
|
||||||
|
<div key={f.name} className={`grid w-[${f.width}] max-w-sm items-center gap-1.5`}>
|
||||||
|
<Label className="text-muted-foreground" htmlFor={f.name}>
|
||||||
|
{f.label}
|
||||||
|
</Label>
|
||||||
|
<Input type="email" id={f.name} name={f.name} required={f.required} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full items-center justify-center">
|
||||||
|
<PopoverForm
|
||||||
|
title={title}
|
||||||
|
open={open}
|
||||||
|
setOpen={setOpen}
|
||||||
|
width="364px"
|
||||||
|
height="auto"
|
||||||
|
showCloseButton={formState !== 'success'}
|
||||||
|
showSuccess={formState === 'success'}
|
||||||
|
openChild={
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="relative p-6 flex flex-col gap-3">{renderFields(fields || [])}</div>
|
||||||
|
<div className="relative flex h-12 items-center px-[10px]">
|
||||||
|
<PopoverFormSeparator />
|
||||||
|
<div className="absolute left-0 top-0 -translate-x-[1.5px] -translate-y-1/2">
|
||||||
|
<PopoverFormCutOutLeftIcon />
|
||||||
|
</div>
|
||||||
|
<div className="absolute right-0 top-0 translate-x-[1.5px] -translate-y-1/2 rotate-180">
|
||||||
|
<PopoverFormCutOutRightIcon />
|
||||||
|
</div>
|
||||||
|
<PopoverFormButton loading={formState === 'loading'} text={submitButtonLabel || ''} />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
successChild={
|
||||||
|
<PopoverFormSuccess
|
||||||
|
title="Feedback Received"
|
||||||
|
description="Thank you for supporting our project!"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
14
src/blocks/ContactForm/config.ts
Normal file
14
src/blocks/ContactForm/config.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Block } from "payload";
|
||||||
|
|
||||||
|
export const ContactForm: Block = {
|
||||||
|
slug: 'contactForm',
|
||||||
|
interfaceName: 'ContactFormBlock',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'form',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: 'forms',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ import { SimpleList } from './SimpleList/component'
|
|||||||
import { Showcase } from './Showcase/component'
|
import { Showcase } from './Showcase/component'
|
||||||
import { ProfileBrief } from './ProfileBrief/component'
|
import { ProfileBrief } from './ProfileBrief/component'
|
||||||
import { BadgeList } from './BadgeList/component'
|
import { BadgeList } from './BadgeList/component'
|
||||||
|
import { ContactForm } from './ContactForm/component'
|
||||||
|
|
||||||
const blockComponents = {
|
const blockComponents = {
|
||||||
simpleBrief: SimpleBrief,
|
simpleBrief: SimpleBrief,
|
||||||
@ -13,6 +14,7 @@ const blockComponents = {
|
|||||||
showcase: Showcase,
|
showcase: Showcase,
|
||||||
profileBrief: ProfileBrief,
|
profileBrief: ProfileBrief,
|
||||||
badgeList: BadgeList,
|
badgeList: BadgeList,
|
||||||
|
contactForm: ContactForm,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RenderBlocks: React.FC<{
|
export const RenderBlocks: React.FC<{
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { BadgeList } from "@/blocks/BadgeList/config";
|
import { BadgeList } from "@/blocks/BadgeList/config";
|
||||||
|
import { ContactForm } from "@/blocks/ContactForm/config";
|
||||||
import { ProfileBrief } from "@/blocks/ProfileBrief/config";
|
import { ProfileBrief } from "@/blocks/ProfileBrief/config";
|
||||||
import { Showcase } from "@/blocks/Showcase/config";
|
import { Showcase } from "@/blocks/Showcase/config";
|
||||||
import { SimpleBrief } from "@/blocks/SimpleBrief/config";
|
import { SimpleBrief } from "@/blocks/SimpleBrief/config";
|
||||||
@ -21,7 +22,7 @@ export const Pages: CollectionConfig = {
|
|||||||
{
|
{
|
||||||
name: 'layout',
|
name: 'layout',
|
||||||
type: 'blocks',
|
type: 'blocks',
|
||||||
blocks: [SimpleList, Showcase, ProfileBrief, SimpleBrief, BadgeList],
|
blocks: [SimpleList, Showcase, ProfileBrief, SimpleBrief, BadgeList, ContactForm],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
104
src/components/ContactForm.tsx
Normal file
104
src/components/ContactForm.tsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import {
|
||||||
|
PopoverForm,
|
||||||
|
PopoverFormButton,
|
||||||
|
PopoverFormCutOutLeftIcon,
|
||||||
|
PopoverFormCutOutRightIcon,
|
||||||
|
PopoverFormSeparator,
|
||||||
|
PopoverFormSuccess,
|
||||||
|
} from './popover-form'
|
||||||
|
|
||||||
|
type FormState = 'idle' | 'loading' | 'success'
|
||||||
|
|
||||||
|
const ContactForm = () => {
|
||||||
|
const [formState, setFormState] = useState<FormState>('idle')
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [feedback, setFeedback] = useState('')
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
setFormState('loading')
|
||||||
|
setTimeout(() => {
|
||||||
|
setFormState('success')
|
||||||
|
}, 1500)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setOpen(false)
|
||||||
|
setFormState('idle')
|
||||||
|
setFeedback('')
|
||||||
|
}, 3300)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(event.ctrlKey || event.metaKey) &&
|
||||||
|
event.key === 'Enter' &&
|
||||||
|
open &&
|
||||||
|
formState === 'idle'
|
||||||
|
) {
|
||||||
|
submit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown)
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||||
|
}, [open, formState])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full items-center justify-center">
|
||||||
|
<PopoverForm
|
||||||
|
title="Feedback"
|
||||||
|
open={open}
|
||||||
|
setOpen={setOpen}
|
||||||
|
width="364px"
|
||||||
|
height="192px"
|
||||||
|
showCloseButton={formState !== 'success'}
|
||||||
|
showSuccess={formState === 'success'}
|
||||||
|
openChild={
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!feedback) return
|
||||||
|
submit()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="relative">
|
||||||
|
<textarea
|
||||||
|
autoFocus
|
||||||
|
placeholder="Feedback"
|
||||||
|
value={feedback}
|
||||||
|
onChange={(e) => setFeedback(e.target.value)}
|
||||||
|
className="h-32 w-full resize-none rounded-t-lg p-3 text-sm outline-none"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="relative flex h-12 items-center px-[10px]">
|
||||||
|
<PopoverFormSeparator />
|
||||||
|
<div className="absolute left-0 top-0 -translate-x-[1.5px] -translate-y-1/2">
|
||||||
|
<PopoverFormCutOutLeftIcon />
|
||||||
|
</div>
|
||||||
|
<div className="absolute right-0 top-0 translate-x-[1.5px] -translate-y-1/2 rotate-180">
|
||||||
|
<PopoverFormCutOutRightIcon />
|
||||||
|
</div>
|
||||||
|
<PopoverFormButton loading={formState === 'loading'} text="" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
successChild={
|
||||||
|
<PopoverFormSuccess
|
||||||
|
title="Feedback Received"
|
||||||
|
description="Thank you for supporting our project!"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContactForm
|
||||||
305
src/components/popover-form.tsx
Normal file
305
src/components/popover-form.tsx
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { ReactNode, RefObject, useEffect, useRef } from 'react'
|
||||||
|
import { ChevronUp, Loader } from 'lucide-react'
|
||||||
|
import { AnimatePresence, motion } from 'motion/react'
|
||||||
|
|
||||||
|
type PopoverFormProps = {
|
||||||
|
open: boolean
|
||||||
|
setOpen: (open: boolean) => void
|
||||||
|
openChild?: ReactNode
|
||||||
|
successChild?: ReactNode
|
||||||
|
showSuccess: boolean
|
||||||
|
width?: string
|
||||||
|
height?: string
|
||||||
|
showCloseButton?: boolean
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PopoverForm({
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
openChild,
|
||||||
|
showSuccess,
|
||||||
|
successChild,
|
||||||
|
width = '364px',
|
||||||
|
height = '192px',
|
||||||
|
title = 'Feedback',
|
||||||
|
showCloseButton = false,
|
||||||
|
}: PopoverFormProps) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
// @ts-expect-error because this was brought in through shadcn and I dont want to worry about it cause it is a nonissue
|
||||||
|
|
||||||
|
useClickOutside(ref, () => setOpen(false))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={title} className="flex w-full items-center justify-center z-50">
|
||||||
|
<motion.button
|
||||||
|
layoutId={`${title}-wrapper`}
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
style={{ borderRadius: 8 }}
|
||||||
|
className="flex h-9 items-center border bg-purple-300 dark:bg-purple-800 px-3 text-sm font-medium outline-none"
|
||||||
|
>
|
||||||
|
<motion.span layoutId={`${title}-title`}>{title}</motion.span>
|
||||||
|
</motion.button>
|
||||||
|
<AnimatePresence>
|
||||||
|
{open && (
|
||||||
|
<motion.div
|
||||||
|
layoutId={`${title}-wrapper`}
|
||||||
|
className="absolute p-1 overflow-hidden bg-muted shadow-2xl outline-none"
|
||||||
|
ref={ref}
|
||||||
|
style={{ borderRadius: 10, width, height }}
|
||||||
|
>
|
||||||
|
<motion.span
|
||||||
|
aria-hidden
|
||||||
|
className="absolute left-4 top-[17px] text-sm text-muted-foreground data-[success]:text-transparent"
|
||||||
|
layoutId={`${title}-title`}
|
||||||
|
data-success={showSuccess}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</motion.span>
|
||||||
|
|
||||||
|
{showCloseButton && (
|
||||||
|
<div className="absolute -top-[5px] left-1/2 transform -translate-x-1/2 w-[12px] h-[26px] flex items-center justify-center z-20">
|
||||||
|
<button
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
className="absolute z-10 -mt-1 flex items-center justify-center w-[10px] h-[6px] text-muted-foreground hover:text-foreground focus:outline-none rounded-full "
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<ChevronUp className="text-muted-foreground/80" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<PopoverFormCutOutTopIcon />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AnimatePresence mode="popLayout">
|
||||||
|
{showSuccess ? (
|
||||||
|
<motion.div
|
||||||
|
key="success"
|
||||||
|
initial={{ y: -32, opacity: 0, filter: 'blur(4px)' }}
|
||||||
|
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
|
||||||
|
transition={{ type: 'spring', duration: 0.4, bounce: 0 }}
|
||||||
|
className="flex h-full flex-col items-center justify-center"
|
||||||
|
>
|
||||||
|
{successChild || <PopoverFormSuccess />}
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
exit={{ y: 8, opacity: 0, filter: 'blur(4px)' }}
|
||||||
|
transition={{ type: 'spring', duration: 0.4, bounce: 0 }}
|
||||||
|
key="open-child"
|
||||||
|
style={{ borderRadius: 10 }}
|
||||||
|
className="h-full border bg-white dark:bg-[#121212] z-20 "
|
||||||
|
>
|
||||||
|
{openChild}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PopoverFormButton({
|
||||||
|
loading,
|
||||||
|
text = 'submit',
|
||||||
|
}: {
|
||||||
|
loading: boolean
|
||||||
|
text: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="ml-auto flex h-6 w-26 items-center justify-center overflow-hidden rounded-md bg-gradient-to-b from-primary/90 to-primary px-3 text-xs font-semibold text-primary-foreground shadow-[0_0_1px_1px_rgba(255,255,255,0.08)_inset,0_1px_1.5px_0_rgba(0,0,0,0.32),0_0_0_0.5px_#1a94ff]"
|
||||||
|
>
|
||||||
|
<AnimatePresence mode="popLayout" initial={false}>
|
||||||
|
<motion.span
|
||||||
|
key={`${loading}`}
|
||||||
|
initial={{ opacity: 0, y: -25 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 25 }}
|
||||||
|
transition={{
|
||||||
|
type: 'spring',
|
||||||
|
duration: 0.3,
|
||||||
|
bounce: 0,
|
||||||
|
}}
|
||||||
|
className="flex w-full items-center justify-center"
|
||||||
|
>
|
||||||
|
{loading ? <Loader className="animate-spin size-3" /> : <span>{text}</span>}
|
||||||
|
</motion.span>
|
||||||
|
</AnimatePresence>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const useClickOutside = (
|
||||||
|
ref: RefObject<HTMLElement>,
|
||||||
|
handleOnClickOutside: (event: MouseEvent | TouchEvent) => void,
|
||||||
|
) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const listener = (event: MouseEvent | TouchEvent) => {
|
||||||
|
if (!ref.current || ref.current.contains(event.target as Node)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleOnClickOutside(event)
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', listener)
|
||||||
|
document.addEventListener('touchstart', listener)
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', listener)
|
||||||
|
document.removeEventListener('touchstart', listener)
|
||||||
|
}
|
||||||
|
}, [ref, handleOnClickOutside])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PopoverFormSuccess({
|
||||||
|
title = 'Success',
|
||||||
|
description = 'Thank you for your submission',
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<svg
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
viewBox="0 0 32 32"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="-mt-1"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M27.6 16C27.6 17.5234 27.3 19.0318 26.717 20.4392C26.1341 21.8465 25.2796 23.1253 24.2025 24.2025C23.1253 25.2796 21.8465 26.1341 20.4392 26.717C19.0318 27.3 17.5234 27.6 16 27.6C14.4767 27.6 12.9683 27.3 11.5609 26.717C10.1535 26.1341 8.87475 25.2796 7.79759 24.2025C6.72043 23.1253 5.86598 21.8465 5.28302 20.4392C4.70007 19.0318 4.40002 17.5234 4.40002 16C4.40002 12.9235 5.62216 9.97301 7.79759 7.79759C9.97301 5.62216 12.9235 4.40002 16 4.40002C19.0765 4.40002 22.027 5.62216 24.2025 7.79759C26.3779 9.97301 27.6 12.9235 27.6 16Z"
|
||||||
|
fill="#2090FF"
|
||||||
|
fillOpacity="0.16"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12.1334 16.9667L15.0334 19.8667L19.8667 13.1M27.6 16C27.6 17.5234 27.3 19.0318 26.717 20.4392C26.1341 21.8465 25.2796 23.1253 24.2025 24.2025C23.1253 25.2796 21.8465 26.1341 20.4392 26.717C19.0318 27.3 17.5234 27.6 16 27.6C14.4767 27.6 12.9683 27.3 11.5609 26.717C10.1535 26.1341 8.87475 25.2796 7.79759 24.2025C6.72043 23.1253 5.86598 21.8465 5.28302 20.4392C4.70007 19.0318 4.40002 17.5234 4.40002 16C4.40002 12.9235 5.62216 9.97301 7.79759 7.79759C9.97301 5.62216 12.9235 4.40002 16 4.40002C19.0765 4.40002 22.027 5.62216 24.2025 7.79759C26.3779 9.97301 27.6 12.9235 27.6 16Z"
|
||||||
|
stroke="#2090FF"
|
||||||
|
strokeWidth="2.4"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<h3 className="mb-1 mt-2 text-sm font-medium text-primary">{title}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground max-w-xs text-pretty mx-auto text-center">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PopoverFormSeparator({
|
||||||
|
width = 352,
|
||||||
|
height = 2,
|
||||||
|
}: {
|
||||||
|
width?: number | string
|
||||||
|
height?: number
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className="absolute left-0 right-0 top-[-1px]"
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
viewBox="0 0 352 2"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path d="M0 1H352" className="stroke-border" strokeDasharray="4 4" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverFormCutOutTopIcon({
|
||||||
|
width = 44,
|
||||||
|
height = 30,
|
||||||
|
}: {
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
}) {
|
||||||
|
const aspectRatio = 6 / 12
|
||||||
|
const calculatedHeight = width * aspectRatio
|
||||||
|
const calculatedWidth = height / aspectRatio
|
||||||
|
|
||||||
|
const finalWidth = Math.min(width, calculatedWidth)
|
||||||
|
const finalHeight = Math.min(height, calculatedHeight)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={finalWidth}
|
||||||
|
height={finalHeight}
|
||||||
|
viewBox="0 0 6 12"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="rotate-90 mt-[1px]"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
>
|
||||||
|
<g clipPath="url(#clip0_2029_22)">
|
||||||
|
<path
|
||||||
|
d="M0 2C0.656613 2 1.30679 2.10346 1.91341 2.30448C2.52005 2.5055 3.07124 2.80014 3.53554 3.17157C3.99982 3.54301 4.36812 3.98396 4.6194 4.46927C4.87067 4.95457 5 5.47471 5 6C5 6.52529 4.87067 7.04543 4.6194 7.53073C4.36812 8.01604 3.99982 8.45699 3.53554 8.82843C3.07124 9.19986 2.52005 9.4945 1.91341 9.69552C1.30679 9.89654 0.656613 10 0 10V6V2Z"
|
||||||
|
className="fill-muted"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M1 12V10C2.06087 10 3.07828 9.57857 3.82843 8.82843C4.57857 8.07828 5 7.06087 5 6C5 4.93913 4.57857 3.92172 3.82843 3.17157C3.07828 2.42143 2.06087 2 1 2V0"
|
||||||
|
className="stroke-border"
|
||||||
|
strokeWidth={0.6}
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_2029_22">
|
||||||
|
<rect width={finalWidth} height={finalHeight} fill="white" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PopoverFormCutOutLeftIcon() {
|
||||||
|
return (
|
||||||
|
<svg width="6" height="12" viewBox="0 0 6 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clipPath="url(#clip0_2029_22)">
|
||||||
|
<path
|
||||||
|
d="M0 2C0.656613 2 1.30679 2.10346 1.91341 2.30448C2.52005 2.5055 3.07124 2.80014 3.53554 3.17157C3.99982 3.54301 4.36812 3.98396 4.6194 4.46927C4.87067 4.95457 5 5.47471 5 6C5 6.52529 4.87067 7.04543 4.6194 7.53073C4.36812 8.01604 3.99982 8.45699 3.53554 8.82843C3.07124 9.19986 2.52005 9.4945 1.91341 9.69552C1.30679 9.89654 0.656613 10 0 10V6V2Z"
|
||||||
|
className="fill-muted"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M1 12V10C2.06087 10 3.07828 9.57857 3.82843 8.82843C4.57857 8.07828 5 7.06087 5 6C5 4.93913 4.57857 3.92172 3.82843 3.17157C3.07828 2.42143 2.06087 2 1 2V0"
|
||||||
|
className="stroke-border"
|
||||||
|
strokeWidth="1"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_2029_22">
|
||||||
|
<rect width="6" height="12" fill="white" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PopoverFormCutOutRightIcon() {
|
||||||
|
return (
|
||||||
|
<svg width="6" height="12" viewBox="0 0 6 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clipPath="url(#clip0_2029_22)">
|
||||||
|
<path
|
||||||
|
d="M0 2C0.656613 2 1.30679 2.10346 1.91341 2.30448C2.52005 2.5055 3.07124 2.80014 3.53554 3.17157C3.99982 3.54301 4.36812 3.98396 4.6194 4.46927C4.87067 4.95457 5 5.47471 5 6C5 6.52529 4.87067 7.04543 4.6194 7.53073C4.36812 8.01604 3.99982 8.45699 3.53554 8.82843C3.07124 9.19986 2.52005 9.4945 1.91341 9.69552C1.30679 9.89654 0.656613 10 0 10V6V2Z"
|
||||||
|
className="fill-muted"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M1 12V10C2.06087 10 3.07828 9.57857 3.82843 8.82843C4.57857 8.07828 5 7.06087 5 6C5 4.93913 4.57857 3.92172 3.82843 3.17157C3.07828 2.42143 2.06087 2 1 2V0"
|
||||||
|
className="stroke-border"
|
||||||
|
strokeWidth="1"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_2029_22">
|
||||||
|
<rect width="6" height="12" fill="white" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -90,9 +90,9 @@ export function ProjectCard({
|
|||||||
<CardContent className="mt-auto flex flex-col px-2">
|
<CardContent className="mt-auto flex flex-col px-2">
|
||||||
{tags?.badges && tags.badges.length > 0 && (
|
{tags?.badges && tags.badges.length > 0 && (
|
||||||
<div className="mt-2 flex flex-wrap gap-1">
|
<div className="mt-2 flex flex-wrap gap-1">
|
||||||
{tags.badges.map((b, i) => (
|
{tags.badges.map((b) => (
|
||||||
<Badge
|
<Badge
|
||||||
key={b.value || '' + i}
|
key={crypto.randomUUID()}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'px-1 py-0 text-[10px]',
|
'px-1 py-0 text-[10px]',
|
||||||
|
|||||||
@ -29,7 +29,7 @@ const BlurFade = ({
|
|||||||
blur = '6px',
|
blur = '6px',
|
||||||
}: BlurFadeProps) => {
|
}: BlurFadeProps) => {
|
||||||
const ref = useRef(null)
|
const ref = useRef(null)
|
||||||
// @ts-ignore
|
// @ts-expect-error cause this is from a shadcn import ant i dont care
|
||||||
const inViewResult = useInView(ref, { once: true, margin: inViewMargin })
|
const inViewResult = useInView(ref, { once: true, margin: inViewMargin })
|
||||||
const isInView = !inView || inViewResult
|
const isInView = !inView || inViewResult
|
||||||
const defaultVariants: Variants = {
|
const defaultVariants: Variants = {
|
||||||
|
|||||||
21
src/components/ui/input.tsx
Normal file
21
src/components/ui/input.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
data-slot="input"
|
||||||
|
className={cn(
|
||||||
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Input }
|
||||||
24
src/components/ui/label.tsx
Normal file
24
src/components/ui/label.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Label({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
data-slot="label"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Label }
|
||||||
25
src/components/ui/sonner.tsx
Normal file
25
src/components/ui/sonner.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useTheme } from "next-themes"
|
||||||
|
import { Toaster as Sonner, ToasterProps } from "sonner"
|
||||||
|
|
||||||
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
|
const { theme = "system" } = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sonner
|
||||||
|
theme={theme as ToasterProps["theme"]}
|
||||||
|
className="toaster group"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--normal-bg": "var(--popover)",
|
||||||
|
"--normal-text": "var(--popover-foreground)",
|
||||||
|
"--normal-border": "var(--border)",
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Toaster }
|
||||||
18
src/components/ui/textarea.tsx
Normal file
18
src/components/ui/textarea.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
data-slot="textarea"
|
||||||
|
className={cn(
|
||||||
|
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Textarea }
|
||||||
@ -70,6 +70,8 @@ export interface Config {
|
|||||||
users: User;
|
users: User;
|
||||||
media: Media;
|
media: Media;
|
||||||
pages: Page;
|
pages: Page;
|
||||||
|
forms: Form;
|
||||||
|
'form-submissions': FormSubmission;
|
||||||
'payload-locked-documents': PayloadLockedDocument;
|
'payload-locked-documents': PayloadLockedDocument;
|
||||||
'payload-preferences': PayloadPreference;
|
'payload-preferences': PayloadPreference;
|
||||||
'payload-migrations': PayloadMigration;
|
'payload-migrations': PayloadMigration;
|
||||||
@ -79,6 +81,8 @@ export interface Config {
|
|||||||
users: UsersSelect<false> | UsersSelect<true>;
|
users: UsersSelect<false> | UsersSelect<true>;
|
||||||
media: MediaSelect<false> | MediaSelect<true>;
|
media: MediaSelect<false> | MediaSelect<true>;
|
||||||
pages: PagesSelect<false> | PagesSelect<true>;
|
pages: PagesSelect<false> | PagesSelect<true>;
|
||||||
|
forms: FormsSelect<false> | FormsSelect<true>;
|
||||||
|
'form-submissions': FormSubmissionsSelect<false> | FormSubmissionsSelect<true>;
|
||||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||||
@ -164,7 +168,9 @@ export interface Media {
|
|||||||
export interface Page {
|
export interface Page {
|
||||||
id: number;
|
id: number;
|
||||||
slug?: string | null;
|
slug?: string | null;
|
||||||
layout?: (SimpleListBlock | ShowcaseBlock | ProfileBriefBlock | SimpleBriefBlock | BadgeListBlock)[] | null;
|
layout?:
|
||||||
|
| (SimpleListBlock | ShowcaseBlock | ProfileBriefBlock | SimpleBriefBlock | BadgeListBlock | ContactFormBlock)[]
|
||||||
|
| null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
@ -317,6 +323,207 @@ export interface SimpleBriefBlock {
|
|||||||
blockName?: string | null;
|
blockName?: string | null;
|
||||||
blockType: 'simpleBrief';
|
blockType: 'simpleBrief';
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "ContactFormBlock".
|
||||||
|
*/
|
||||||
|
export interface ContactFormBlock {
|
||||||
|
form: number | Form;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'contactForm';
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "forms".
|
||||||
|
*/
|
||||||
|
export interface Form {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
fields?:
|
||||||
|
| (
|
||||||
|
| {
|
||||||
|
name: string;
|
||||||
|
label?: string | null;
|
||||||
|
width?: number | null;
|
||||||
|
required?: boolean | null;
|
||||||
|
defaultValue?: boolean | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'checkbox';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name: string;
|
||||||
|
label?: string | null;
|
||||||
|
width?: number | null;
|
||||||
|
required?: boolean | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'country';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name: string;
|
||||||
|
label?: string | null;
|
||||||
|
width?: number | null;
|
||||||
|
required?: boolean | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'email';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
message?: {
|
||||||
|
root: {
|
||||||
|
type: string;
|
||||||
|
children: {
|
||||||
|
type: string;
|
||||||
|
version: number;
|
||||||
|
[k: string]: unknown;
|
||||||
|
}[];
|
||||||
|
direction: ('ltr' | 'rtl') | null;
|
||||||
|
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||||
|
indent: number;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
[k: string]: unknown;
|
||||||
|
} | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'message';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name: string;
|
||||||
|
label?: string | null;
|
||||||
|
width?: number | null;
|
||||||
|
defaultValue?: number | null;
|
||||||
|
required?: boolean | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'number';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name: string;
|
||||||
|
label?: string | null;
|
||||||
|
width?: number | null;
|
||||||
|
defaultValue?: string | null;
|
||||||
|
placeholder?: string | null;
|
||||||
|
options?:
|
||||||
|
| {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
required?: boolean | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'select';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name: string;
|
||||||
|
label?: string | null;
|
||||||
|
width?: number | null;
|
||||||
|
required?: boolean | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'state';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name: string;
|
||||||
|
label?: string | null;
|
||||||
|
width?: number | null;
|
||||||
|
defaultValue?: string | null;
|
||||||
|
required?: boolean | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'text';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name: string;
|
||||||
|
label?: string | null;
|
||||||
|
width?: number | null;
|
||||||
|
defaultValue?: string | null;
|
||||||
|
required?: boolean | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'textarea';
|
||||||
|
}
|
||||||
|
)[]
|
||||||
|
| null;
|
||||||
|
submitButtonLabel?: string | null;
|
||||||
|
/**
|
||||||
|
* Choose whether to display an on-page message or redirect to a different page after they submit the form.
|
||||||
|
*/
|
||||||
|
confirmationType?: ('message' | 'redirect') | null;
|
||||||
|
confirmationMessage?: {
|
||||||
|
root: {
|
||||||
|
type: string;
|
||||||
|
children: {
|
||||||
|
type: string;
|
||||||
|
version: number;
|
||||||
|
[k: string]: unknown;
|
||||||
|
}[];
|
||||||
|
direction: ('ltr' | 'rtl') | null;
|
||||||
|
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||||
|
indent: number;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
[k: string]: unknown;
|
||||||
|
} | null;
|
||||||
|
redirect?: {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Send custom emails when the form submits. Use comma separated lists to send the same email to multiple recipients. To reference a value from this form, wrap that field's name with double curly brackets, i.e. {{firstName}}. You can use a wildcard {{*}} to output all data and {{*:table}} to format it as an HTML table in the email.
|
||||||
|
*/
|
||||||
|
emails?:
|
||||||
|
| {
|
||||||
|
emailTo?: string | null;
|
||||||
|
cc?: string | null;
|
||||||
|
bcc?: string | null;
|
||||||
|
replyTo?: string | null;
|
||||||
|
emailFrom?: string | null;
|
||||||
|
subject: string;
|
||||||
|
/**
|
||||||
|
* Enter the message that should be sent in this email.
|
||||||
|
*/
|
||||||
|
message?: {
|
||||||
|
root: {
|
||||||
|
type: string;
|
||||||
|
children: {
|
||||||
|
type: string;
|
||||||
|
version: number;
|
||||||
|
[k: string]: unknown;
|
||||||
|
}[];
|
||||||
|
direction: ('ltr' | 'rtl') | null;
|
||||||
|
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||||
|
indent: number;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
[k: string]: unknown;
|
||||||
|
} | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "form-submissions".
|
||||||
|
*/
|
||||||
|
export interface FormSubmission {
|
||||||
|
id: number;
|
||||||
|
form: number | Form;
|
||||||
|
submissionData?:
|
||||||
|
| {
|
||||||
|
field: string;
|
||||||
|
value: string;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "payload-locked-documents".
|
* via the `definition` "payload-locked-documents".
|
||||||
@ -335,6 +542,14 @@ export interface PayloadLockedDocument {
|
|||||||
| ({
|
| ({
|
||||||
relationTo: 'pages';
|
relationTo: 'pages';
|
||||||
value: number | Page;
|
value: number | Page;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'forms';
|
||||||
|
value: number | Form;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'form-submissions';
|
||||||
|
value: number | FormSubmission;
|
||||||
} | null);
|
} | null);
|
||||||
globalSlug?: string | null;
|
globalSlug?: string | null;
|
||||||
user: {
|
user: {
|
||||||
@ -425,6 +640,7 @@ export interface PagesSelect<T extends boolean = true> {
|
|||||||
profileBrief?: T | ProfileBriefBlockSelect<T>;
|
profileBrief?: T | ProfileBriefBlockSelect<T>;
|
||||||
simpleBrief?: T | SimpleBriefBlockSelect<T>;
|
simpleBrief?: T | SimpleBriefBlockSelect<T>;
|
||||||
badgeList?: T | BadgeListBlockSelect<T>;
|
badgeList?: T | BadgeListBlockSelect<T>;
|
||||||
|
contactForm?: T | ContactFormBlockSelect<T>;
|
||||||
};
|
};
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
@ -536,6 +752,164 @@ export interface SimpleBriefBlockSelect<T extends boolean = true> {
|
|||||||
id?: T;
|
id?: T;
|
||||||
blockName?: T;
|
blockName?: T;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "ContactFormBlock_select".
|
||||||
|
*/
|
||||||
|
export interface ContactFormBlockSelect<T extends boolean = true> {
|
||||||
|
form?: T;
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "forms_select".
|
||||||
|
*/
|
||||||
|
export interface FormsSelect<T extends boolean = true> {
|
||||||
|
title?: T;
|
||||||
|
fields?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
checkbox?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
name?: T;
|
||||||
|
label?: T;
|
||||||
|
width?: T;
|
||||||
|
required?: T;
|
||||||
|
defaultValue?: T;
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
};
|
||||||
|
country?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
name?: T;
|
||||||
|
label?: T;
|
||||||
|
width?: T;
|
||||||
|
required?: T;
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
};
|
||||||
|
email?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
name?: T;
|
||||||
|
label?: T;
|
||||||
|
width?: T;
|
||||||
|
required?: T;
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
};
|
||||||
|
message?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
message?: T;
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
};
|
||||||
|
number?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
name?: T;
|
||||||
|
label?: T;
|
||||||
|
width?: T;
|
||||||
|
defaultValue?: T;
|
||||||
|
required?: T;
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
};
|
||||||
|
select?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
name?: T;
|
||||||
|
label?: T;
|
||||||
|
width?: T;
|
||||||
|
defaultValue?: T;
|
||||||
|
placeholder?: T;
|
||||||
|
options?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
label?: T;
|
||||||
|
value?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
required?: T;
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
};
|
||||||
|
state?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
name?: T;
|
||||||
|
label?: T;
|
||||||
|
width?: T;
|
||||||
|
required?: T;
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
};
|
||||||
|
text?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
name?: T;
|
||||||
|
label?: T;
|
||||||
|
width?: T;
|
||||||
|
defaultValue?: T;
|
||||||
|
required?: T;
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
};
|
||||||
|
textarea?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
name?: T;
|
||||||
|
label?: T;
|
||||||
|
width?: T;
|
||||||
|
defaultValue?: T;
|
||||||
|
required?: T;
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
submitButtonLabel?: T;
|
||||||
|
confirmationType?: T;
|
||||||
|
confirmationMessage?: T;
|
||||||
|
redirect?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
url?: T;
|
||||||
|
};
|
||||||
|
emails?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
emailTo?: T;
|
||||||
|
cc?: T;
|
||||||
|
bcc?: T;
|
||||||
|
replyTo?: T;
|
||||||
|
emailFrom?: T;
|
||||||
|
subject?: T;
|
||||||
|
message?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "form-submissions_select".
|
||||||
|
*/
|
||||||
|
export interface FormSubmissionsSelect<T extends boolean = true> {
|
||||||
|
form?: T;
|
||||||
|
submissionData?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
field?: T;
|
||||||
|
value?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "payload-locked-documents_select".
|
* via the `definition` "payload-locked-documents_select".
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import { s3Storage } from '@payloadcms/storage-s3'
|
import { s3Storage } from '@payloadcms/storage-s3'
|
||||||
|
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
|
||||||
|
import { formBuilderPlugin } from '@payloadcms/plugin-form-builder'
|
||||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||||
import { payloadCloudPlugin } from '@payloadcms/payload-cloud'
|
import { payloadCloudPlugin } from '@payloadcms/payload-cloud'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
@ -35,6 +37,19 @@ export default buildConfig({
|
|||||||
connectionString: process.env.DATABASE_URI || '',
|
connectionString: process.env.DATABASE_URI || '',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
email: nodemailerAdapter({
|
||||||
|
defaultFromAddress: 'no-reply@beitzah.net',
|
||||||
|
defaultFromName: 'no-reply Beitzah',
|
||||||
|
transportOptions: {
|
||||||
|
host: process.env.SMTP_HOST,
|
||||||
|
port: parseInt(process.env.SMTP_PORT || '', 10),
|
||||||
|
auth: {
|
||||||
|
user: process.env.SMTP_USER,
|
||||||
|
pass: process.env.SMTP_PASS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
sharp,
|
sharp,
|
||||||
plugins: [
|
plugins: [
|
||||||
payloadCloudPlugin(),
|
payloadCloudPlugin(),
|
||||||
@ -52,6 +67,27 @@ export default buildConfig({
|
|||||||
endpoint: process.env.S3_ENDPOINT,
|
endpoint: process.env.S3_ENDPOINT,
|
||||||
forcePathStyle: true,
|
forcePathStyle: true,
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
|
formBuilderPlugin({
|
||||||
|
fields: {
|
||||||
|
text: true,
|
||||||
|
textarea: true,
|
||||||
|
select: true,
|
||||||
|
email: true,
|
||||||
|
state: true,
|
||||||
|
country: true,
|
||||||
|
checkbox: true,
|
||||||
|
number: true,
|
||||||
|
message: true,
|
||||||
|
payment: false,
|
||||||
|
},
|
||||||
|
defaultToEmail: process.env.DEFAULT_EMAIL,
|
||||||
|
formOverrides: {
|
||||||
|
access: {
|
||||||
|
read: () => true, // !!user, // authenticated users only
|
||||||
|
update: () => true,
|
||||||
|
},
|
||||||
|
}
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user