chore: migrate to payload / nextjs

This commit is contained in:
syntaxbullet
2026-06-16 13:02:47 +02:00
parent aff090b693
commit 6b395e5735
257 changed files with 33616 additions and 1451 deletions

10
.editorconfig Normal file
View File

@@ -0,0 +1,10 @@
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
end_of_line = lf
max_line_length = null

17
.env.example Normal file
View File

@@ -0,0 +1,17 @@
# Database connection string
DATABASE_URL=mongodb://127.0.0.1/your-database-name
# Or use a PG connection string
#DATABASE_URL=postgresql://127.0.0.1:5432/your-database-name
# Used to encrypt JWT tokens
PAYLOAD_SECRET=YOUR_SECRET_HERE
# Used to configure CORS, format links and more. No trailing slash
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
# Secret used to authenticate cron jobs
CRON_SECRET=YOUR_CRON_SECRET_HERE
# Used to validate preview requests
PREVIEW_SECRET=YOUR_SECRET_HERE

26
.gitignore vendored
View File

@@ -1,6 +1,22 @@
node_modules/
dist/
public/extracted_logos/
*.timestamp-*.mjs
build
dist / media
node_modules
.DS_Store
.pi
.env
.next
next-env.d.ts
.vercel
# Payload default media upload directory
public/media/
public/robots.txt
public/sitemap*.xml
# Playwright
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

2
.npmrc Normal file
View File

@@ -0,0 +1,2 @@
legacy-peer-deps=true
enable-pre-post-scripts=true

9
.pi/auth.json Normal file
View File

@@ -0,0 +1,9 @@
{
"openai-codex": {
"type": "oauth",
"access": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE5MzQ0ZTY1LWJiYzktNDRkMS1hOWQwLWY5NTdiMDc5YmQwZSIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS92MSJdLCJjbGllbnRfaWQiOiJhcHBfRU1vYW1FRVo3M2YwQ2tYYVhwN2hyYW5uIiwiZXhwIjoxNzgyMTE3MzgxLCJodHRwczovL2FwaS5vcGVuYWkuY29tL2F1dGgiOnsiY2hhdGdwdF9hY2NvdW50X2lkIjoiMDJjZDFlOGMtMzViZi00ODkxLTgyYTktOTkyY2RiZTFkNTZlIiwiY2hhdGdwdF9hY2NvdW50X3VzZXJfaWQiOiJ1c2VyLXFqNHVwSVJ1b25sTEdPTUZYRVNUOXNxOF9fMDJjZDFlOGMtMzViZi00ODkxLTgyYTktOTkyY2RiZTFkNTZlIiwiY2hhdGdwdF9jb21wdXRlX3Jlc2lkZW5jeSI6Im5vX2NvbnN0cmFpbnQiLCJjaGF0Z3B0X3BsYW5fdHlwZSI6InByb2xpdGUiLCJjaGF0Z3B0X3VzZXJfaWQiOiJ1c2VyLXFqNHVwSVJ1b25sTEdPTUZYRVNUOXNxOCIsImxvY2FsaG9zdCI6dHJ1ZSwidXNlcl9pZCI6InVzZXItcWo0dXBJUnVvbmxMR09NRlhFU1Q5c3E4In0sImh0dHBzOi8vYXBpLm9wZW5haS5jb20vcHJvZmlsZSI6eyJlbWFpbCI6Iml2aXNjcmlwdHNAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWV9LCJpYXQiOjE3ODEyNTMzODAsImlzcyI6Imh0dHBzOi8vYXV0aC5vcGVuYWkuY29tIiwianRpIjoiNmUzNmQ1ZDEtZTVmYi00YWMwLWI2ZjQtYTgyZmU3M2JhNGNmIiwibmJmIjoxNzgxMjUzMzgwLCJwd2RfYXV0aF90aW1lIjoxNzgxMjUzMzc5ODA0LCJzY3AiOlsib3BlbmlkIiwicHJvZmlsZSIsImVtYWlsIiwib2ZmbGluZV9hY2Nlc3MiXSwic2Vzc2lvbl9pZCI6ImF1dGhzZXNzXzdtUkllRWpicHk0OHJDOGJVRVlIM2hqOCIsInNsIjp0cnVlLCJzdWIiOiJnb29nbGUtb2F1dGgyfDExNTU5MjY0ODIxMjU4MzQ5NzgzMCJ9.ZVEDhcFG8jbJdwIMJcNdBwZQtLFXfqFHscHUReBan0VeDyrwdNi5PWPaRX_IinmthoNML5iCVOf9chpIYXh3OTsuI2V3r-5Cb8sMm7I54xm6oDPo_DQ5NEZPFBxRAyGlM0iI2rlb6rrc_Rqhn34UyvLrFSK8xJyQ94GR4P5RmYYGS8kMP0gghY4Eg8nKw_uTJ1c4DSpYOzSYbM3ocdp7J9pKt-oOosLLv4TdLsO6fu0gY8TGnDJ8WkpgmHSTsCYWG32fzRKq3gJzEhXkTQB72nuNYpbjFhWRYxAU4Wc2Lu6cfo-ulqa_CXCV6j_5gd9G-yCfQC2S5McgL6lT4XRE955mL5FPXlmu0A__IKDn1qbd0PqAX5XyO9vP7yp2kUFZO0a4QqEpsfoWLM_TjAf8dgD8_IlClDDRxlSMBl_ex6xPre208ion_WvJXv68tCxQnAQKrdEUoqRkD3gawvXjo7_GpJZqzldJS7Nb_s7ZVJrwfumu7Vg8NHUHfKSOdIJv36jtDhqxcklYEhpsqsIDFkiJttGr2aYmJioztISkc5KaVQBhnnRTmcrH1ui2mB4KHcjldv_u9ArdOX09WWxo2h-uDcyKFXThsGyNS-fT9_acFGlH1iL5gG5EvokxT_v_FeysufrrOy_NGHeGEMhvoJYqpvQtyYIJ7wD_fOwjD3E",
"refresh": "rt.1.AAA-V8eXajSoeYFAkKuxM-B3FbeINU-bE9c0FFX_V3rl628PMBaM23IGydwbMqKRX6MuO5HeD8AevAyaOevN5e2LRj3hX0DKPjXzy0BMY0CjgY11fBZIewkNH5e11c3c-L9M7-jooIDcHvzOo_AmG1YQa2JpExX5lYRiL230GuAlTFZOg7vyZlyu3jixjdF1ZRslY7sbIOljJitITbXyf-0s0dmElZaHrMK8NKDPGWoTUgo9qYrx1XRNGazpNcOStkoCAsk-M9XZXcskUJGbuAwJFyC04MLLPE6Be80-6FY8pmWYiFCFiR4OM-ctu4AWXDQ",
"expires": 1782117380801,
"accountId": "02cd1e8c-35bf-4891-82a9-992cdbe1d56e"
}
}

6
.pi/settings.json Normal file
View File

@@ -0,0 +1,6 @@
{
"lastChangelogVersion": "0.79.1",
"defaultProvider": "openai-codex",
"defaultModel": "gpt-5.5",
"defaultThinkingLevel": "medium"
}

View File

@@ -0,0 +1,149 @@
---
name: cms-migration
description: Use when user wants to migrate content from another CMS (WordPress, Contentful, Strapi, Sanity, Webflow, etc.) to Payload CMS
---
# CMS Migration to Payload
Interactive workflow to design Payload collections from source CMS data. Config-first approach: establish the data structure through conversation before any data import.
## Workflow
```
Start
↓
Ask for data sample
↓
Analyze data shape
↓
Propose collection config
↓
User reviews ──────────────┐
│ │
├─ changes needed ───→ Adjust config ──→ (back to User reviews)
│
└─ looks good ───→ Config confirmed
↓
More collections? ──────┐
│ │
├─ yes ──→ (back to Ask for data sample)
│
└─ no ───→ All collections confirmed
↓
Discuss migration approach
↓
Done
```
## Phase 1: Data Analysis
When user provides data (JSON, CSV, or describes their schema):
1. **Identify field types** - text, number, date, relationships, media, rich text
2. **Spot patterns** - IDs, timestamps, nested objects, arrays
3. **Note relationships** - foreign keys, embedded refs, linked content types
4. **Flag ambiguities** - fields that could be multiple types, unclear purposes
## Phase 2: Propose Collection Config
Present a Payload collection config based on analysis:
```typescript
// Example output format
export const Posts: CollectionConfig = {
slug: 'posts',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'content', type: 'richText' },
{ name: 'author', type: 'relationship', relationTo: 'users' },
// ...
],
}
```
Explain your reasoning for each field choice. When something could go multiple ways (group vs JSON, text vs textarea, select vs relationship), ask rather than assume.
## Phase 3: Iterate with User
Work through uncertainties: required fields, hasMany relationships, rich text vs HTML, custom timestamps vs built-in. Continue until the user confirms the config.
## Phase 4: Additional Collections
After each confirmation, ask:
> "Are there other content types we should create collections for?"
If yes, loop back to Phase 1 with new data sample.
Common related collections to prompt for:
- Media/uploads
- Users/authors
- Categories/tags
- Settings (global)
## Phase 5: Migration Approach
Only after ALL collections are confirmed, discuss data import:
1. **Order matters** - which collections have no dependencies? Migrate those first
2. **Relationship mapping** - how to resolve source IDs to Payload IDs
3. **Media handling** - download/re-upload vs external URLs
4. **Rich text** - HTML conversion needs or keep raw
Offer to generate a seed script or walk through manual import.
## Things to Clarify
Throughout the process, watch for these:
- **ID references** - are they relationships to other collections?
- **Image/file URLs** - upload fields or keep as external URLs?
- **Nested objects** - group, array, or blocks?
- **Localization** - any fields need per-locale values?
- **Access control** - who can read/write this collection?
- **Related content types** - categories, tags, authors that need their own collections?
## Critical: Select vs Relationship
**This is the most common migration mistake.** Data that looks static often needs to be dynamic.
When you see repeated string values (categories, tags, types, statuses):
```json
{ "category": "Technology" }
{ "category": "News" }
{ "category": "Technology" }
```
**Don't assume it's a select field.** Ask:
> "I see `category` has values like 'Technology', 'News'. Should this be:
> - A **select field** with fixed options (values won't change)
> - A **relationship** to a Categories collection (users can add/edit/remove categories later)"
**Default to relationship** for anything that looks like:
- Categories, tags, topics, labels
- Authors, assignees, reviewers
- Statuses beyond simple draft/published
- Types that might expand over time
**Use select only for:**
- Truly fixed enums (yes/no, draft/published/archived)
- Options defined by business logic, not content (payment status, priority levels)
- Values that would break functionality if changed (role types with code dependencies)
If creating a relationship, remember to add the related collection (Categories, Tags, etc.) to the migration plan.
## Reference Documentation
- **[PAYLOAD-FIELD-REFERENCE.md](reference/PAYLOAD-FIELD-REFERENCE.md)** - Complete Payload field type schemas with examples
## Common Pitfalls
| Issue | How to Handle |
|-------|---------------|
| User provides partial data | Ask for more samples, especially edge cases |
| Unclear relationships | Ask user to describe how content types connect |
| Rich text ambiguity | Clarify: Lexical editor, Slate, or store raw HTML |
| Missing media collection | Always confirm upload collection exists before referencing |
| Overly complex nested data | Consider flattening or using blocks instead of deep groups |

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,57 @@
# Payload Skill for AI Coding Agents
Agent skill providing comprehensive guidance for Payload development with TypeScript patterns, field configurations, hooks, access control, and API examples.
## What's Included
The `payload` skill provides expert guidance on:
- **Collections**: Auth, uploads, drafts, live preview configurations
- **Fields**: All field types including relationships, arrays, blocks, joins, virtual fields
- **Hooks**: beforeChange, afterChange, beforeValidate, field hooks
- **Access Control**: Collection, field, and global access patterns including RBAC and multi-tenant
- **Queries**: Local API, REST, and GraphQL with complex operators
- **Database Adapters**: MongoDB, Postgres, SQLite configurations and transactions
- **Advanced Features**: Jobs queue, custom endpoints, localization, plugins
## Usage
Once installed, the Agent will automatically invoke the skill when you're working on Payload CMS projects. The skill activates when you:
- Edit `payload.config.ts` files
- Work with collection or global configurations
- Ask about Payload-specific patterns
- Need guidance on fields, hooks, or access control
You can also explicitly invoke it:
```
@payload how do I implement row-level access control?
```
## Documentation Structure
```
skills/payload/
├── SKILL.md # Main skill file with quick reference
└── reference/
├── FIELDS.md # All field types and configurations
├── COLLECTIONS.md # Collection patterns
├── HOOKS.md # Hook patterns and examples
├── ACCESS-CONTROL.md # Basic access control
├── ACCESS-CONTROL-ADVANCED.md # Advanced access patterns
├── QUERIES.md # Query patterns and APIs
├── ADAPTERS.md # Database and storage adapters
└── ADVANCED.md # Jobs, endpoints, localization
```
## Resources
- [Payload Documentation](https://payloadcms.com/docs)
- [GitHub Repository](https://github.com/payloadcms/payload)
- [Examples](https://github.com/payloadcms/payload/tree/main/examples)
- [Templates](https://github.com/payloadcms/payload/tree/main/templates)
## License
MIT

409
.pi/skills/payload/SKILL.md Normal file
View File

@@ -0,0 +1,409 @@
---
name: payload
description: Use when working with Payload projects (payload.config.ts, collections, fields, hooks, access control, Payload API). Use when debugging validation errors, security issues, relationship queries, transactions, or hook behavior.
---
# Payload Application Development
Payload is a Next.js native CMS with TypeScript-first architecture, providing admin panel, database management, REST/GraphQL APIs, authentication, and file storage.
## Quick Reference
| Task | Solution | Details |
| ------------------------ | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Auto-generate slugs | `slugField()` | [FIELDS.md#slug-field-helper](reference/FIELDS.md#slug-field-helper) |
| Restrict content by user | Access control with query | [ACCESS-CONTROL.md#row-level-security-with-complex-queries](reference/ACCESS-CONTROL.md#row-level-security-with-complex-queries) |
| Local API user ops | `user` + `overrideAccess: false` | [QUERIES.md#access-control-in-local-api](reference/QUERIES.md#access-control-in-local-api) |
| Draft/publish workflow | `versions: { drafts: true }` | [COLLECTIONS.md#versioning--drafts](reference/COLLECTIONS.md#versioning--drafts) |
| Computed fields | `virtual: true` with afterRead | [FIELDS.md#virtual-fields](reference/FIELDS.md#virtual-fields) |
| Conditional fields | `admin.condition` | [FIELDS.md#conditional-fields](reference/FIELDS.md#conditional-fields) |
| Custom field validation | `validate` function | [FIELDS.md#text-field](reference/FIELDS.md#text-field) |
| Filter relationship list | `filterOptions` on field | [FIELDS.md#relationship](reference/FIELDS.md#relationship) |
| Select specific fields | `select` parameter | [QUERIES.md#local-api](reference/QUERIES.md#local-api) |
| Auto-set author/dates | beforeChange hook | [HOOKS.md#collection-hooks](reference/HOOKS.md#collection-hooks) |
| Prevent hook loops | `req.context` check | [HOOKS.md#hook-context](reference/HOOKS.md#hook-context) |
| Cascading deletes | beforeDelete hook | [HOOKS.md#collection-hooks](reference/HOOKS.md#collection-hooks) |
| Geospatial queries | `point` field with `near`/`within` | [FIELDS.md#point-geolocation](reference/FIELDS.md#point-geolocation) |
| Reverse relationships | `join` field type | [FIELDS.md#join-fields](reference/FIELDS.md#join-fields) |
| Next.js revalidation | Context control in afterChange | [HOOKS.md#nextjs-revalidation-with-context-control](reference/HOOKS.md#nextjs-revalidation-with-context-control) |
| Query by relationship | Nested property syntax | [QUERIES.md#nested-properties](reference/QUERIES.md#nested-properties) |
| Complex queries | AND/OR logic | [QUERIES.md#andor-logic](reference/QUERIES.md#andor-logic) |
| Transactions | Pass `req` to operations | [ADAPTERS.md#threading-req-through-operations](reference/ADAPTERS.md#threading-req-through-operations) |
| Background jobs | Jobs queue with tasks | [ADVANCED.md#jobs-queue](reference/ADVANCED.md#jobs-queue) |
| Custom API routes | Collection custom endpoints | [ADVANCED.md#custom-endpoints](reference/ADVANCED.md#custom-endpoints) |
| Cloud storage | Storage adapter plugins | [ADAPTERS.md#storage-adapters](reference/ADAPTERS.md#storage-adapters) |
| Multi-language | `localization` config + `localized: true` | [ADVANCED.md#localization](reference/ADVANCED.md#localization) |
| Create plugin | `(options) => (config) => Config` | [PLUGIN-DEVELOPMENT.md#plugin-architecture](reference/PLUGIN-DEVELOPMENT.md#plugin-architecture) |
| Plugin package setup | Package structure with SWC | [PLUGIN-DEVELOPMENT.md#plugin-package-structure](reference/PLUGIN-DEVELOPMENT.md#plugin-package-structure) |
| Add fields to collection | Map collections, spread fields | [PLUGIN-DEVELOPMENT.md#adding-fields-to-collections](reference/PLUGIN-DEVELOPMENT.md#adding-fields-to-collections) |
| Plugin hooks | Preserve existing hooks in array | [PLUGIN-DEVELOPMENT.md#adding-hooks](reference/PLUGIN-DEVELOPMENT.md#adding-hooks) |
| Check field type | Type guard functions | [FIELD-TYPE-GUARDS.md](reference/FIELD-TYPE-GUARDS.md) |
## Quick Start
```bash
npx create-payload-app@latest my-app
cd my-app
pnpm dev
```
### Minimal Config
```ts
import { buildConfig } from 'payload'
import { mongooseAdapter } from '@payloadcms/db-mongodb'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import path from 'path'
import { fileURLToPath } from 'url'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export default buildConfig({
admin: {
user: 'users',
importMap: {
baseDir: path.resolve(dirname),
},
},
collections: [Users, Media],
editor: lexicalEditor(),
secret: process.env.PAYLOAD_SECRET,
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
db: mongooseAdapter({
url: process.env.DATABASE_URL,
}),
})
```
## Essential Patterns
### Basic Collection
```ts
import type { CollectionConfig } from 'payload'
export const Posts: CollectionConfig = {
slug: 'posts',
admin: {
useAsTitle: 'title',
defaultColumns: ['title', 'author', 'status', 'createdAt'],
},
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'slug', type: 'text', unique: true, index: true },
{ name: 'content', type: 'richText' },
{ name: 'author', type: 'relationship', relationTo: 'users' },
],
timestamps: true,
}
```
For more collection patterns (auth, upload, drafts, live preview), see [COLLECTIONS.md](reference/COLLECTIONS.md).
### Common Fields
```ts
// Text field
{ name: 'title', type: 'text', required: true }
// Relationship
{ name: 'author', type: 'relationship', relationTo: 'users', required: true }
// Rich text
{ name: 'content', type: 'richText', required: true }
// Select
{ name: 'status', type: 'select', options: ['draft', 'published'], defaultValue: 'draft' }
// Upload
{ name: 'image', type: 'upload', relationTo: 'media' }
```
For all field types (array, blocks, point, join, virtual, conditional, etc.), see [FIELDS.md](reference/FIELDS.md).
### Hook Example
```ts
export const Posts: CollectionConfig = {
slug: 'posts',
hooks: {
beforeChange: [
async ({ data, operation }) => {
if (operation === 'create') {
data.slug = slugify(data.title)
}
return data
},
],
},
fields: [{ name: 'title', type: 'text' }],
}
```
For all hook patterns, see [HOOKS.md](reference/HOOKS.md). For access control, see [ACCESS-CONTROL.md](reference/ACCESS-CONTROL.md).
### Access Control with Type Safety
```ts
import type { Access } from 'payload'
import type { User } from '@/payload-types'
// Type-safe access control
export const adminOnly: Access = ({ req }) => {
const user = req.user as User
return user?.roles?.includes('admin') || false
}
// Row-level access control
export const ownPostsOnly: Access = ({ req }) => {
const user = req.user as User
if (!user) return false
if (user.roles?.includes('admin')) return true
return {
author: { equals: user.id },
}
}
```
### Query Example
```ts
// Local API
const posts = await payload.find({
collection: 'posts',
where: {
status: { equals: 'published' },
'author.name': { contains: 'john' },
},
depth: 2,
limit: 10,
sort: '-createdAt',
})
// Query with populated relationships
const post = await payload.findByID({
collection: 'posts',
id: '123',
depth: 2, // Populates relationships (default is 2)
})
// Returns: { author: { id: "user123", name: "John" } }
// Without depth, relationships return IDs only
const post = await payload.findByID({
collection: 'posts',
id: '123',
depth: 0,
})
// Returns: { author: "user123" }
```
For all query operators and REST/GraphQL examples, see [QUERIES.md](reference/QUERIES.md).
### Getting Payload Instance
```ts
// In API routes (Next.js)
import { getPayload } from 'payload'
import config from '@payload-config'
export async function GET() {
const payload = await getPayload({ config })
const posts = await payload.find({
collection: 'posts',
})
return Response.json(posts)
}
// In Server Components
import { getPayload } from 'payload'
import config from '@payload-config'
export default async function Page() {
const payload = await getPayload({ config })
const { docs } = await payload.find({ collection: 'posts' })
return <div>{docs.map(post => <h1 key={post.id}>{post.title}</h1>)}</div>
}
```
### Logger Usage
```ts
// ✅ Valid: single string
payload.logger.error('Something went wrong')
// ✅ Valid: object with msg and err
payload.logger.error({ msg: 'Failed to process', err: error })
// ❌ Invalid: don't pass error as second argument
payload.logger.error('Failed to process', error)
// ❌ Invalid: use `err` not `error`, use `msg` not `message`
payload.logger.error({ message: 'Failed', error: error })
```
## Security Pitfalls
### 1. Local API Access Control (CRITICAL)
**By default, Local API operations bypass ALL access control**, even when passing a user.
```ts
// ❌ SECURITY BUG: Passes user but ignores their permissions
await payload.find({
collection: 'posts',
user: someUser, // Access control is BYPASSED!
})
// ✅ SECURE: Actually enforces the user's permissions
await payload.find({
collection: 'posts',
user: someUser,
overrideAccess: false, // REQUIRED for access control
})
```
**When to use each:**
- `overrideAccess: true` (default) - Server-side operations you trust (cron jobs, system tasks)
- `overrideAccess: false` - When operating on behalf of a user (API routes, webhooks)
See [QUERIES.md#access-control-in-local-api](reference/QUERIES.md#access-control-in-local-api).
### 2. Transaction Failures in Hooks
**Nested operations in hooks without `req` break transaction atomicity.**
```ts
// ❌ DATA CORRUPTION RISK: Separate transaction
hooks: {
afterChange: [
async ({ doc, req }) => {
await req.payload.create({
collection: 'audit-log',
data: { docId: doc.id },
// Missing req - runs in separate transaction!
})
},
]
}
// ✅ ATOMIC: Same transaction
hooks: {
afterChange: [
async ({ doc, req }) => {
await req.payload.create({
collection: 'audit-log',
data: { docId: doc.id },
req, // Maintains atomicity
})
},
]
}
```
See [ADAPTERS.md#threading-req-through-operations](reference/ADAPTERS.md#threading-req-through-operations).
### 3. Infinite Hook Loops
**Hooks triggering operations that trigger the same hooks create infinite loops.**
```ts
// ❌ INFINITE LOOP
hooks: {
afterChange: [
async ({ doc, req }) => {
await req.payload.update({
collection: 'posts',
id: doc.id,
data: { views: doc.views + 1 },
req,
}) // Triggers afterChange again!
},
]
}
// ✅ SAFE: Use context flag
hooks: {
afterChange: [
async ({ doc, req, context }) => {
if (context.skipHooks) return
await req.payload.update({
collection: 'posts',
id: doc.id,
data: { views: doc.views + 1 },
context: { skipHooks: true },
req,
})
},
]
}
```
See [HOOKS.md#context](reference/HOOKS.md#context).
## Project Structure
```txt
src/
├── app/
│ ├── (frontend)/
│ │ └── page.tsx
│ └── (payload)/
│ └── admin/[[...segments]]/page.tsx
├── collections/
│ ├── Posts.ts
│ ├── Media.ts
│ └── Users.ts
├── globals/
│ └── Header.ts
├── components/
│ └── CustomField.tsx
├── hooks/
│ └── slugify.ts
└── payload.config.ts
```
## Type Generation
```ts
// payload.config.ts
export default buildConfig({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
// ...
})
// Usage
import type { Post, User } from '@/payload-types'
```
## Reference Documentation
- **[FIELDS.md](reference/FIELDS.md)** - All field types, validation, admin options
- **[FIELD-TYPE-GUARDS.md](reference/FIELD-TYPE-GUARDS.md)** - Type guards for runtime field type checking and narrowing
- **[COLLECTIONS.md](reference/COLLECTIONS.md)** - Collection configs, auth, upload, drafts, live preview
- **[HOOKS.md](reference/HOOKS.md)** - Collection hooks, field hooks, context patterns
- **[ACCESS-CONTROL.md](reference/ACCESS-CONTROL.md)** - Collection, field, global access control, RBAC, multi-tenant
- **[ACCESS-CONTROL-ADVANCED.md](reference/ACCESS-CONTROL-ADVANCED.md)** - Context-aware, time-based, subscription-based access, factory functions, templates
- **[QUERIES.md](reference/QUERIES.md)** - Query operators, Local/REST/GraphQL APIs
- **[ENDPOINTS.md](reference/ENDPOINTS.md)** - Custom API endpoints: authentication, helpers, request/response patterns
- **[ADAPTERS.md](reference/ADAPTERS.md)** - Database, storage, email adapters, transactions
- **[ADVANCED.md](reference/ADVANCED.md)** - Authentication, jobs, endpoints, components, plugins, localization
- **[PLUGIN-DEVELOPMENT.md](reference/PLUGIN-DEVELOPMENT.md)** - Plugin architecture, monorepo structure, patterns, best practices
## Resources
- llms-full.txt: <https://payloadcms.com/llms-full.txt>
- Docs: <https://payloadcms.com/docs>
- GitHub: <https://github.com/payloadcms/payload>
- Examples: <https://github.com/payloadcms/payload/tree/main/examples>
- Templates: <https://github.com/payloadcms/payload/tree/main/templates>

View File

@@ -0,0 +1,704 @@
# Payload Access Control - Advanced Patterns
Advanced access control patterns including context-aware access, time-based restrictions, factory functions, and production templates.
## Context-Aware Access Patterns
### Locale-Specific Access
Control access based on user locale for internationalized content.
```ts
import type { Access } from 'payload'
export const localeSpecificAccess: Access = ({ req: { user, locale } }) => {
// Authenticated users can access all locales
if (user) return true
// Public users can only access English content
if (locale === 'en') return true
return false
}
// Usage in collection
export const Posts: CollectionConfig = {
slug: 'posts',
access: {
read: localeSpecificAccess,
},
fields: [{ name: 'title', type: 'text', localized: true }],
}
```
**Source**: `docs/access-control/overview.mdx` (req.locale argument)
### Device-Specific Access
Restrict access based on device type or user agent.
```ts
import type { Access } from 'payload'
export const mobileOnlyAccess: Access = ({ req: { headers } }) => {
const userAgent = headers?.get('user-agent') || ''
return /mobile|android|iphone/i.test(userAgent)
}
export const desktopOnlyAccess: Access = ({ req: { headers } }) => {
const userAgent = headers?.get('user-agent') || ''
return !/mobile|android|iphone/i.test(userAgent)
}
// Usage
export const MobileContent: CollectionConfig = {
slug: 'mobile-content',
access: {
read: mobileOnlyAccess,
},
fields: [{ name: 'title', type: 'text' }],
}
```
**Source**: Synthesized (headers pattern)
### IP-Based Access
Restrict access from specific IP addresses (requires middleware/proxy headers).
```ts
import type { Access } from 'payload'
export const restrictedIpAccess = (allowedIps: string[]): Access => {
return ({ req: { headers } }) => {
const ip = headers?.get('x-forwarded-for') || headers?.get('x-real-ip')
return allowedIps.includes(ip || '')
}
}
// Usage
const internalIps = ['192.168.1.0/24', '10.0.0.5']
export const InternalDocs: CollectionConfig = {
slug: 'internal-docs',
access: {
read: restrictedIpAccess(internalIps),
},
fields: [{ name: 'content', type: 'richText' }],
}
```
**Note**: Requires your server to pass IP address via headers (common with proxies/load balancers).
**Source**: Synthesized (headers pattern)
## Time-Based Access Patterns
### Today's Records Only
```ts
import type { Access } from 'payload'
export const todayOnlyAccess: Access = ({ req: { user } }) => {
if (!user) return false
const now = new Date()
const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const endOfDay = new Date(startOfDay.getTime() + 24 * 60 * 60 * 1000)
return {
createdAt: {
greater_than_equal: startOfDay.toISOString(),
less_than: endOfDay.toISOString(),
},
}
}
```
**Source**: `test/access-control/config.ts` (query constraint patterns)
### Recent Records (Last N Days)
```ts
import type { Access } from 'payload'
export const recentRecordsAccess = (days: number): Access => {
return ({ req: { user } }) => {
if (!user) return false
if (user.roles?.includes('admin')) return true
const cutoff = new Date()
cutoff.setDate(cutoff.getDate() - days)
return {
createdAt: {
greater_than_equal: cutoff.toISOString(),
},
}
}
}
// Usage: Users see only last 30 days, admins see all
export const Logs: CollectionConfig = {
slug: 'logs',
access: {
read: recentRecordsAccess(30),
},
fields: [{ name: 'message', type: 'text' }],
}
```
### Scheduled Content (Publish Date Range)
```ts
import type { Access } from 'payload'
export const scheduledContentAccess: Access = ({ req: { user } }) => {
// Editors see all content
if (user?.roles?.includes('admin') || user?.roles?.includes('editor')) {
return true
}
const now = new Date().toISOString()
// Public sees only content within publish window
return {
and: [
{ publishDate: { less_than_equal: now } },
{
or: [{ unpublishDate: { exists: false } }, { unpublishDate: { greater_than: now } }],
},
],
}
}
```
**Source**: Synthesized (query constraint + date patterns)
## Subscription-Based Access
### Active Subscription Required
```ts
import type { Access } from 'payload'
export const activeSubscriptionAccess: Access = async ({ req: { user } }) => {
if (!user) return false
if (user.roles?.includes('admin')) return true
try {
const subscription = await req.payload.findByID({
collection: 'subscriptions',
id: user.subscriptionId,
})
return subscription?.status === 'active'
} catch {
return false
}
}
// Usage
export const PremiumContent: CollectionConfig = {
slug: 'premium-content',
access: {
read: activeSubscriptionAccess,
},
fields: [{ name: 'title', type: 'text' }],
}
```
### Subscription Tier-Based Access
```ts
import type { Access } from 'payload'
export const tierBasedAccess = (requiredTier: string): Access => {
const tierHierarchy = ['free', 'basic', 'pro', 'enterprise']
return async ({ req: { user } }) => {
if (!user) return false
if (user.roles?.includes('admin')) return true
try {
const subscription = await req.payload.findByID({
collection: 'subscriptions',
id: user.subscriptionId,
})
if (subscription?.status !== 'active') return false
const userTierIndex = tierHierarchy.indexOf(subscription.tier)
const requiredTierIndex = tierHierarchy.indexOf(requiredTier)
return userTierIndex >= requiredTierIndex
} catch {
return false
}
}
}
// Usage
export const EnterpriseFeatures: CollectionConfig = {
slug: 'enterprise-features',
access: {
read: tierBasedAccess('enterprise'),
},
fields: [{ name: 'feature', type: 'text' }],
}
```
**Source**: Synthesized (async + cross-collection pattern)
## Factory Functions
Reusable functions that generate access control configurations.
### createRoleBasedAccess
Generate access control for specific roles.
```ts
import type { Access } from 'payload'
export function createRoleBasedAccess(roles: string[]): Access {
return ({ req: { user } }) => {
if (!user) return false
return roles.some((role) => user.roles?.includes(role))
}
}
// Usage
const adminOrEditor = createRoleBasedAccess(['admin', 'editor'])
const moderatorAccess = createRoleBasedAccess(['admin', 'moderator'])
export const Posts: CollectionConfig = {
slug: 'posts',
access: {
create: adminOrEditor,
update: adminOrEditor,
delete: moderatorAccess,
},
fields: [{ name: 'title', type: 'text' }],
}
```
**Source**: `test/access-control/config.ts`
### createOrgScopedAccess
Generate organization-scoped access with optional admin bypass.
```ts
import type { Access } from 'payload'
export function createOrgScopedAccess(allowAdmin = true): Access {
return ({ req: { user } }) => {
if (!user) return false
if (allowAdmin && user.roles?.includes('admin')) return true
return {
organizationId: { in: user.organizationIds || [] },
}
}
}
// Usage
const orgScoped = createOrgScopedAccess() // Admins bypass
const strictOrgScoped = createOrgScopedAccess(false) // Admins also scoped
export const Projects: CollectionConfig = {
slug: 'projects',
access: {
read: orgScoped,
update: orgScoped,
delete: strictOrgScoped,
},
fields: [
{ name: 'title', type: 'text' },
{ name: 'organizationId', type: 'text', required: true },
],
}
```
**Source**: `test/access-control/config.ts`
### createTeamBasedAccess
Generate team-scoped access with configurable field name.
```ts
import type { Access } from 'payload'
export function createTeamBasedAccess(teamField = 'teamId'): Access {
return ({ req: { user } }) => {
if (!user) return false
if (user.roles?.includes('admin')) return true
return {
[teamField]: { in: user.teamIds || [] },
}
}
}
// Usage with custom field name
const projectTeamAccess = createTeamBasedAccess('projectTeam')
export const Tasks: CollectionConfig = {
slug: 'tasks',
access: {
read: projectTeamAccess,
update: projectTeamAccess,
},
fields: [
{ name: 'title', type: 'text' },
{ name: 'projectTeam', type: 'text', required: true },
],
}
```
**Source**: Synthesized (org pattern variation)
### createTimeLimitedAccess
Generate access limited to records within specified days.
```ts
import type { Access } from 'payload'
export function createTimeLimitedAccess(daysAccess: number): Access {
return ({ req: { user } }) => {
if (!user) return false
if (user.roles?.includes('admin')) return true
const cutoff = new Date()
cutoff.setDate(cutoff.getDate() - daysAccess)
return {
createdAt: {
greater_than_equal: cutoff.toISOString(),
},
}
}
}
// Usage: Users see 90 days, admins see all
export const ActivityLogs: CollectionConfig = {
slug: 'activity-logs',
access: {
read: createTimeLimitedAccess(90),
},
fields: [{ name: 'action', type: 'text' }],
}
```
**Source**: Synthesized (time + query pattern)
## Configuration Templates
Complete collection configurations for common scenarios.
### Basic Authenticated Collection
```ts
import type { CollectionConfig } from 'payload'
export const BasicCollection: CollectionConfig = {
slug: 'basic-collection',
access: {
create: ({ req: { user } }) => Boolean(user),
read: ({ req: { user } }) => Boolean(user),
update: ({ req: { user } }) => Boolean(user),
delete: ({ req: { user } }) => Boolean(user),
},
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'content', type: 'richText' },
],
}
```
**Source**: `docs/access-control/collections.mdx`
### Public + Authenticated Collection
```ts
import type { CollectionConfig } from 'payload'
export const PublicAuthCollection: CollectionConfig = {
slug: 'posts',
access: {
// Only admins/editors can create
create: ({ req: { user } }) => {
return user?.roles?.some((role) => ['admin', 'editor'].includes(role)) || false
},
// Authenticated users see all, public sees only published
read: ({ req: { user } }) => {
if (user) return true
return { _status: { equals: 'published' } }
},
// Only admins/editors can update
update: ({ req: { user } }) => {
return user?.roles?.some((role) => ['admin', 'editor'].includes(role)) || false
},
// Only admins can delete
delete: ({ req: { user } }) => {
return user?.roles?.includes('admin') || false
},
},
versions: {
drafts: true,
},
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'content', type: 'richText', required: true },
{ name: 'author', type: 'relationship', relationTo: 'users' },
],
}
```
**Source**: `templates/website/src/collections/Posts/index.ts`
### Multi-User/Self-Service Collection
```ts
import type { CollectionConfig } from 'payload'
export const SelfServiceCollection: CollectionConfig = {
slug: 'users',
auth: true,
access: {
// Admins can create users
create: ({ req: { user } }) => user?.roles?.includes('admin') || false,
// Anyone can read user profiles
read: () => true,
// Users can update self, admins can update anyone
update: ({ req: { user }, id }) => {
if (!user) return false
if (user.roles?.includes('admin')) return true
return user.id === id
},
// Only admins can delete
delete: ({ req: { user } }) => user?.roles?.includes('admin') || false,
},
fields: [
{ name: 'name', type: 'text', required: true },
{ name: 'email', type: 'email', required: true },
{
name: 'roles',
type: 'select',
hasMany: true,
options: ['admin', 'editor', 'user'],
access: {
// Only admins can read/update roles
read: ({ req: { user } }) => user?.roles?.includes('admin') || false,
update: ({ req: { user } }) => user?.roles?.includes('admin') || false,
},
},
],
}
```
**Source**: `templates/website/src/collections/Users/index.ts`
## Debugging Tips
### Log Access Check Execution
```ts
export const debugAccess: Access = ({ req: { user }, id }) => {
console.log('Access check:', {
userId: user?.id,
userRoles: user?.roles,
docId: id,
timestamp: new Date().toISOString(),
})
return true
}
```
### Verify Arguments Availability
```ts
export const checkArgsAccess: Access = (args) => {
console.log('Available arguments:', {
hasReq: 'req' in args,
hasUser: args.req?.user ? 'yes' : 'no',
hasId: args.id ? 'provided' : 'undefined',
hasData: args.data ? 'provided' : 'undefined',
})
return true
}
```
### Measure Async Operation Timing
```ts
export const timedAsyncAccess: Access = async ({ req }) => {
const start = Date.now()
const result = await fetch('https://auth-service.example.com/validate', {
headers: { userId: req.user?.id },
})
console.log(`Access check took ${Date.now() - start}ms`)
return result.ok
}
```
### Test Access Without User
```ts
// In test/development
const testAccess = await payload.find({
collection: 'posts',
overrideAccess: false, // Enforce access control
user: undefined, // Simulate no user
})
console.log('Public access result:', testAccess.docs.length)
```
**Source**: Synthesized (debugging best practices)
## Performance Considerations
### Async Operations Impact
```ts
// ❌ Slow: Multiple sequential async calls
export const slowAccess: Access = async ({ req: { user } }) => {
const org = await req.payload.findByID({ collection: 'orgs', id: user.orgId })
const team = await req.payload.findByID({ collection: 'teams', id: user.teamId })
const subscription = await req.payload.findByID({ collection: 'subs', id: user.subId })
return org.active && team.active && subscription.active
}
// ✅ Fast: Use query constraints or cache in context
export const fastAccess: Access = ({ req: { user, context } }) => {
// Cache expensive lookups
if (!context.orgStatus) {
context.orgStatus = checkOrgStatus(user.orgId)
}
return context.orgStatus
}
```
### Query Constraint Optimization
```ts
// ❌ Avoid: Non-indexed fields in constraints
export const slowQuery: Access = () => ({
'metadata.internalCode': { equals: 'ABC123' }, // Slow if not indexed
})
// ✅ Better: Use indexed fields
export const fastQuery: Access = () => ({
status: { equals: 'active' }, // Indexed field
organizationId: { in: ['org1', 'org2'] }, // Indexed field
})
```
### Field Access on Large Arrays
```ts
// ❌ Slow: Complex access on array fields
const arrayField: ArrayField = {
name: 'items',
type: 'array',
fields: [
{
name: 'secretData',
type: 'text',
access: {
read: async ({ req }) => {
// Async call runs for EVERY array item
const result = await expensiveCheck()
return result
},
},
},
],
}
// ✅ Fast: Simple checks or cache result
const optimizedArrayField: ArrayField = {
name: 'items',
type: 'array',
fields: [
{
name: 'secretData',
type: 'text',
access: {
read: ({ req: { user }, context }) => {
// Cache once, reuse for all items
if (context.canReadSecret === undefined) {
context.canReadSecret = user?.roles?.includes('admin')
}
return context.canReadSecret
},
},
},
],
}
```
### Avoid N+1 Queries
```ts
// ❌ N+1 Problem: Query per access check
export const n1Access: Access = async ({ req, id }) => {
// Runs for EACH document in list
const doc = await req.payload.findByID({ collection: 'docs', id })
return doc.isPublic
}
// ✅ Better: Use query constraint to filter at DB level
export const efficientAccess: Access = () => {
return { isPublic: { equals: true } }
}
```
**Performance Best Practices:**
1. **Minimize Async Operations**: Use query constraints over async lookups when possible
2. **Cache Expensive Checks**: Store results in `req.context` for reuse
3. **Index Query Fields**: Ensure fields in query constraints are indexed
4. **Avoid Complex Logic in Array Fields**: Simple boolean checks preferred
5. **Use Query Constraints**: Let database filter rather than loading all records
**Source**: Synthesized (operational best practices)
## Enhanced Best Practices
Comprehensive security and implementation guidelines:
1. **Default Deny**: Start with restrictive access, gradually add permissions
2. **Type Guards**: Use TypeScript for user type safety and better IDE support
3. **Validate Data**: Never trust frontend-provided IDs or data
4. **Async for Critical Checks**: Use async operations for important security decisions
5. **Consistent Logic**: Apply same rules at field and collection levels
6. **Test Edge Cases**: Test with no user, wrong user, admin user scenarios
7. **Monitor Access**: Log failed access attempts for security review
8. **Regular Audit**: Review access rules quarterly or after major changes
9. **Cache Wisely**: Use `req.context` for expensive operations
10. **Document Intent**: Add comments explaining complex access rules
11. **Avoid Secrets in Client**: Never expose sensitive logic to client-side
12. **Rate Limit External Calls**: Protect against DoS on external validation services
13. **Handle Errors Gracefully**: Access functions should return `false` on error, not throw
14. **Use Environment Vars**: Store configuration (IPs, API keys) in env vars
15. **Test Local API**: Remember to set `overrideAccess: false` when testing
16. **Consider Performance**: Measure impact of async operations on login time
17. **Version Control**: Track access control changes in git history
18. **Principle of Least Privilege**: Grant minimum access required for functionality
**Sources**: `docs/access-control/*.mdx`, synthesized best practices

View File

@@ -0,0 +1,697 @@
# Payload Access Control Reference
Complete reference for access control patterns across collections, fields, and globals.
## At a Glance
| Feature | Scope | Returns | Use Case |
| --------------------- | --------------------------------------------------------- | ---------------------- | ---------------------------------- |
| **Collection Access** | create, read, update, delete, admin, unlock, readVersions | boolean \| Where query | Document-level permissions |
| **Field Access** | create, read, update | boolean only | Field-level visibility/editability |
| **Global Access** | read, update, readVersions | boolean \| Where query | Global document permissions |
## Three Layers of Access Control
Payload provides three distinct access control layers:
1. **Collection-Level**: Controls operations on entire documents (create, read, update, delete, admin, unlock, readVersions)
2. **Field-Level**: Controls access to individual fields (create, read, update)
3. **Global-Level**: Controls access to global documents (read, update, readVersions)
## Return Value Types
Access control functions can return:
- **Boolean**: `true` (allow) or `false` (deny)
- **Query Constraint**: `Where` object for row-level security (collection-level only)
Field-level access does NOT support query constraints - only boolean returns.
## Operation Decision Tree
```txt
User makes request
│
├─ Collection access check
│ ├─ Returns false? → Deny entire operation
│ ├─ Returns true? → Continue
│ └─ Returns Where? → Apply query constraint
│
├─ Field access check (if applicable)
│ ├─ Returns false? → Field omitted from result
│ └─ Returns true? → Include field
│
└─ Operation completed
```
## Collection Access Control
### Basic Patterns
```ts
import type { CollectionConfig, Access } from 'payload'
export const Posts: CollectionConfig = {
slug: 'posts',
access: {
// Boolean: Only authenticated users can create
create: ({ req: { user } }) => Boolean(user),
// Query constraint: Public sees published, users see all
read: ({ req: { user } }) => {
if (user) return true
return { status: { equals: 'published' } }
},
// User-specific: Admins or document owner
update: ({ req: { user }, id }) => {
if (user?.roles?.includes('admin')) return true
return { author: { equals: user?.id } }
},
// Async: Check related data
delete: async ({ req, id }) => {
const hasComments = await req.payload.count({
collection: 'comments',
where: { post: { equals: id } },
})
return hasComments === 0
},
// Admin panel visibility
admin: ({ req: { user } }) => {
return user?.roles?.includes('admin') || user?.roles?.includes('editor')
},
},
fields: [
{ name: 'title', type: 'text' },
{ name: 'status', type: 'select', options: ['draft', 'published'] },
{ name: 'author', type: 'relationship', relationTo: 'users' },
],
}
```
### Role-Based Access Control (RBAC) Pattern
Payload does NOT provide a roles system by default. The following is a commonly accepted pattern for implementing role-based access control in auth collections:
```ts
import type { CollectionConfig } from 'payload'
export const Users: CollectionConfig = {
slug: 'users',
auth: true,
fields: [
{ name: 'name', type: 'text', required: true },
{ name: 'email', type: 'email', required: true },
{
name: 'roles',
type: 'select',
hasMany: true,
options: ['admin', 'editor', 'user'],
defaultValue: ['user'],
required: true,
// Save roles to JWT for access control without database lookups
saveToJWT: true,
access: {
// Only admins can update roles
update: ({ req: { user } }) => user?.roles?.includes('admin'),
},
},
],
}
```
**Important Notes:**
1. **Not Built-In**: Payload does not provide a roles system out of the box. You must add a `roles` field to your auth collection.
2. **Save to JWT**: Use `saveToJWT: true` to include roles in the JWT token, enabling role checks without database queries.
3. **Default Value**: Set a `defaultValue` to automatically assign new users a default role.
4. **Access Control**: Restrict who can modify roles (typically only admins).
5. **Role Options**: Define your own role hierarchy based on your application needs.
**Using Roles in Access Control:**
```ts
import type { Access } from 'payload'
// Check for specific role
export const adminOnly: Access = ({ req: { user } }) => {
return user?.roles?.includes('admin')
}
// Check for multiple roles
export const adminOrEditor: Access = ({ req: { user } }) => {
return Boolean(user?.roles?.some((role) => ['admin', 'editor'].includes(role)))
}
// Role hierarchy check
export const hasMinimumRole: Access = ({ req: { user } }, minRole: string) => {
const roleHierarchy = ['user', 'editor', 'admin']
const userHighestRole = Math.max(...(user?.roles?.map((r) => roleHierarchy.indexOf(r)) || [-1]))
const requiredRoleIndex = roleHierarchy.indexOf(minRole)
return userHighestRole >= requiredRoleIndex
}
```
### Reusable Access Functions
```ts
import type { Access } from 'payload'
// Anyone (public)
export const anyone: Access = () => true
// Authenticated only
export const authenticated: Access = ({ req: { user } }) => Boolean(user)
// Authenticated or published content
export const authenticatedOrPublished: Access = ({ req: { user } }) => {
if (user) return true
return { _status: { equals: 'published' } }
}
// Admin only
export const admins: Access = ({ req: { user } }) => {
return user?.roles?.includes('admin')
}
// Admin or editor
export const adminsOrEditors: Access = ({ req: { user } }) => {
return Boolean(user?.roles?.some((role) => ['admin', 'editor'].includes(role)))
}
// Self or admin
export const adminsOrSelf: Access = ({ req: { user } }) => {
if (user?.roles?.includes('admin')) return true
return { id: { equals: user?.id } }
}
// Usage
export const Posts: CollectionConfig = {
slug: 'posts',
access: {
create: authenticated,
read: authenticatedOrPublished,
update: adminsOrEditors,
delete: admins,
},
fields: [{ name: 'title', type: 'text' }],
}
```
### Row-Level Security with Complex Queries
```ts
import type { Access } from 'payload'
// Organization-scoped access
export const organizationScoped: Access = ({ req: { user } }) => {
if (user?.roles?.includes('admin')) return true
// Users see only their organization's data
return {
organization: {
equals: user?.organization,
},
}
}
// Multiple conditions with AND
export const complexAccess: Access = ({ req: { user } }) => {
return {
and: [
{ status: { equals: 'published' } },
{ 'author.isActive': { equals: true } },
{
or: [{ visibility: { equals: 'public' } }, { author: { equals: user?.id } }],
},
],
}
}
// Team-based access
export const teamMemberAccess: Access = ({ req: { user } }) => {
if (!user) return false
if (user.roles?.includes('admin')) return true
return {
'team.members': {
contains: user.id,
},
}
}
```
### Header-Based Access (API Keys)
```ts
import type { Access } from 'payload'
export const apiKeyAccess: Access = ({ req }) => {
const apiKey = req.headers.get('x-api-key')
if (!apiKey) return false
// Validate against stored keys
return apiKey === process.env.VALID_API_KEY
}
// Bearer token validation
export const bearerTokenAccess: Access = async ({ req }) => {
const auth = req.headers.get('authorization')
if (!auth?.startsWith('Bearer ')) return false
const token = auth.slice(7)
const isValid = await validateToken(token)
return isValid
}
```
## Field Access Control
Field access does NOT support query constraints - only boolean returns.
### Basic Field Access
```ts
import type { NumberField, FieldAccess } from 'payload'
const salaryReadAccess: FieldAccess = ({ req: { user }, doc }) => {
// Self can read own salary
if (user?.id === doc?.id) return true
// Admin can read all salaries
return user?.roles?.includes('admin')
}
const salaryUpdateAccess: FieldAccess = ({ req: { user } }) => {
// Only admins can update salary
return user?.roles?.includes('admin')
}
const salaryField: NumberField = {
name: 'salary',
type: 'number',
access: {
read: salaryReadAccess,
update: salaryUpdateAccess,
},
}
```
### Sibling Data Access
```ts
import type { ArrayField, FieldAccess } from 'payload'
const contentReadAccess: FieldAccess = ({ req: { user }, siblingData }) => {
// Authenticated users see all
if (user) return true
// Public sees only if marked public
return siblingData?.isPublic === true
}
const arrayField: ArrayField = {
name: 'sections',
type: 'array',
fields: [
{
name: 'isPublic',
type: 'checkbox',
defaultValue: false,
},
{
name: 'content',
type: 'text',
access: {
read: contentReadAccess,
},
},
],
}
```
### Nested Field Access
```ts
import type { GroupField, FieldAccess } from 'payload'
const internalOnlyAccess: FieldAccess = ({ req: { user } }) => {
return user?.roles?.includes('admin') || user?.roles?.includes('internal')
}
const groupField: GroupField = {
name: 'internalMetadata',
type: 'group',
access: {
read: internalOnlyAccess,
update: internalOnlyAccess,
},
fields: [
{ name: 'internalNotes', type: 'textarea' },
{ name: 'priority', type: 'select', options: ['low', 'medium', 'high'] },
],
}
```
### Hiding Admin Fields
```ts
import type { CollectionConfig } from 'payload'
export const Users: CollectionConfig = {
slug: 'users',
auth: true,
fields: [
{ name: 'name', type: 'text', required: true },
{ name: 'email', type: 'email', required: true },
{
name: 'roles',
type: 'select',
hasMany: true,
options: ['admin', 'editor', 'user'],
access: {
// Hide from UI, but still saved/queried
read: ({ req: { user } }) => user?.roles?.includes('admin'),
// Only admins can update roles
update: ({ req: { user } }) => user?.roles?.includes('admin'),
},
},
],
}
```
## Global Access Control
```ts
import type { GlobalConfig, Access } from 'payload'
const adminOnly: Access = ({ req: { user } }) => {
return user?.roles?.includes('admin')
}
export const SiteSettings: GlobalConfig = {
slug: 'site-settings',
access: {
read: () => true, // Anyone can read settings
update: adminOnly, // Only admins can update
readVersions: adminOnly, // Only admins can see version history
},
fields: [
{ name: 'siteName', type: 'text' },
{ name: 'maintenanceMode', type: 'checkbox' },
],
}
```
## Multi-Tenant Access Control
```ts
import type { Access, CollectionConfig } from 'payload'
// Add tenant field to user type
interface User {
id: string
tenantId: string
roles?: string[]
}
// Tenant-scoped access
const tenantAccess: Access = ({ req: { user } }) => {
// No user = no access
if (!user) return false
// Super admin sees all
if (user.roles?.includes('super-admin')) return true
// Users see only their tenant's data
return {
tenant: {
equals: (user as User).tenantId,
},
}
}
export const Posts: CollectionConfig = {
slug: 'posts',
access: {
create: tenantAccess,
read: tenantAccess,
update: tenantAccess,
delete: tenantAccess,
},
fields: [
{ name: 'title', type: 'text' },
{
name: 'tenant',
type: 'text',
required: true,
access: {
// Tenant field hidden from non-admins
update: ({ req: { user } }) => user?.roles?.includes('super-admin'),
},
hooks: {
// Auto-set tenant on create
beforeChange: [
({ req, operation, value }) => {
if (operation === 'create' && !value) {
return (req.user as User)?.tenantId
}
return value
},
],
},
},
],
}
```
## Auth Collection Patterns
### Self or Admin Pattern
```ts
import type { CollectionConfig } from 'payload'
export const Users: CollectionConfig = {
slug: 'users',
auth: true,
access: {
// Anyone can read user profiles
read: () => true,
// Users can update themselves, admins can update anyone
update: ({ req: { user }, id }) => {
if (user?.roles?.includes('admin')) return true
return user?.id === id
},
// Only admins can delete
delete: ({ req: { user } }) => user?.roles?.includes('admin'),
},
fields: [
{ name: 'name', type: 'text' },
{ name: 'email', type: 'email' },
],
}
```
### Restrict Self-Updates
```ts
import type { CollectionConfig, FieldAccess } from 'payload'
const preventSelfRoleChange: FieldAccess = ({ req: { user }, id }) => {
// Admins can change anyone's roles
if (user?.roles?.includes('admin')) return true
// Users cannot change their own roles
if (user?.id === id) return false
return false
}
export const Users: CollectionConfig = {
slug: 'users',
auth: true,
fields: [
{
name: 'roles',
type: 'select',
hasMany: true,
options: ['admin', 'editor', 'user'],
access: {
update: preventSelfRoleChange,
},
},
],
}
```
## Cross-Collection Validation
```ts
import type { Access } from 'payload'
// Check if user is a project member before allowing access
export const projectMemberAccess: Access = async ({ req, id }) => {
const { user, payload } = req
if (!user) return false
if (user.roles?.includes('admin')) return true
// Check if document exists and user is member
const project = await payload.findByID({
collection: 'projects',
id: id as string,
depth: 0,
})
return project.members?.includes(user.id)
}
// Prevent deletion if document has dependencies
export const preventDeleteWithDependencies: Access = async ({ req, id }) => {
const { payload } = req
const dependencyCount = await payload.count({
collection: 'related-items',
where: {
parent: { equals: id },
},
})
return dependencyCount === 0
}
```
## Access Control Function Arguments
### Collection Create
```ts
create: ({ req, data }) => boolean | Where
// req: PayloadRequest
// - req.user: Authenticated user (if any)
// - req.payload: Payload instance for queries
// - req.headers: Request headers
// - req.locale: Current locale
// data: The data being created
```
### Collection Read
```ts
read: ({ req, id }) => boolean | Where
// req: PayloadRequest
// id: Document ID being read
// - undefined during Access Operation (login check)
// - string when reading specific document
```
### Collection Update
```ts
update: ({ req, id, data }) => boolean | Where
// req: PayloadRequest
// id: Document ID being updated
// data: New values being applied
```
### Collection Delete
```ts
delete: ({ req, id }) => boolean | Where
// req: PayloadRequest
// id: Document ID being deleted
```
### Field Create
```ts
access: {
create: ({ req, data, siblingData }) => boolean
}
// req: PayloadRequest
// data: Full document data
// siblingData: Adjacent field values at same level
```
### Field Read
```ts
access: {
read: ({ req, id, doc, siblingData }) => boolean
}
// req: PayloadRequest
// id: Document ID
// doc: Full document
// siblingData: Adjacent field values
```
### Field Update
```ts
access: {
update: ({ req, id, data, doc, siblingData }) => boolean
}
// req: PayloadRequest
// id: Document ID
// data: New values
// doc: Current document
// siblingData: Adjacent field values
```
## Important Notes
1. **Local API Default**: Access control is **skipped by default** in Local API (`overrideAccess: true`). When passing a `user` parameter, you almost always want to set `overrideAccess: false` to respect that user's permissions:
```ts
// ❌ WRONG: Passes user but bypasses access control (default behavior)
await payload.find({
collection: 'posts',
user: someUser, // User is ignored for access control!
})
// ✅ CORRECT: Respects the user's permissions
await payload.find({
collection: 'posts',
user: someUser,
overrideAccess: false, // Required to enforce access control
})
```
**Why this matters**: If you pass `user` without `overrideAccess: false`, the operation runs with admin privileges regardless of the user's actual permissions. This is a common security mistake.
2. **Field Access Limitations**: Field-level access does NOT support query constraints - only boolean returns.
3. **Admin Panel Visibility**: The `admin` access control determines if a collection appears in the admin panel for a user.
4. **Access Before Hooks**: Access control executes BEFORE hooks run, so hooks cannot modify access behavior.
5. **Query Constraints**: Only collection-level `read` access supports query constraints. All other operations and field-level access require boolean returns.
## Best Practices
1. **Reusable Functions**: Create named access functions for common patterns
2. **Fail Secure**: Default to `false` for sensitive operations
3. **Cache Checks**: Use `req.context` to cache expensive validation
4. **Type Safety**: Type your user object for better IDE support
5. **Test Thoroughly**: Write tests for complex access control logic
6. **Document Intent**: Add comments explaining access rules
7. **Audit Logs**: Track access control decisions for security review
8. **Performance**: Avoid N+1 queries in access functions
9. **Error Handling**: Access functions should not throw - return `false` instead
10. **Tenant Hooks**: Auto-set tenant fields in `beforeChange` hooks
## Advanced Patterns
For advanced access control patterns including context-aware access, time-based restrictions, subscription-based access, factory functions, configuration templates, debugging tips, and performance optimization, see [ACCESS-CONTROL-ADVANCED.md](ACCESS-CONTROL-ADVANCED.md).

View File

@@ -0,0 +1,326 @@
# Payload Adapters Reference
Complete reference for database, storage, and email adapters.
## Database Adapters
### MongoDB
```ts
import { mongooseAdapter } from '@payloadcms/db-mongodb'
export default buildConfig({
db: mongooseAdapter({
url: process.env.DATABASE_URL,
}),
})
```
### Postgres
```ts
import { postgresAdapter } from '@payloadcms/db-postgres'
export default buildConfig({
db: postgresAdapter({
pool: {
connectionString: process.env.DATABASE_URL,
},
push: false, // Don't auto-push schema changes
migrationDir: './migrations',
}),
})
```
### SQLite
```ts
import { sqliteAdapter } from '@payloadcms/db-sqlite'
export default buildConfig({
db: sqliteAdapter({
client: {
url: 'file:./payload.db',
},
transactionOptions: {}, // Enable transactions (disabled by default)
}),
})
```
## Transactions
Payload automatically uses transactions for all-or-nothing database operations. Pass `req` to include operations in the same transaction.
```ts
import type { CollectionAfterChangeHook } from 'payload'
const afterChange: CollectionAfterChangeHook = async ({ req, doc }) => {
// This will be part of the same transaction
await req.payload.create({
req, // Pass req to use same transaction
collection: 'audit-log',
data: { action: 'created', docId: doc.id },
})
}
// Manual transaction control
const transactionID = await payload.db.beginTransaction()
try {
await payload.create({
collection: 'orders',
data: orderData,
req: { transactionID },
})
await payload.update({
collection: 'inventory',
id: itemId,
data: { stock: newStock },
req: { transactionID },
})
await payload.db.commitTransaction(transactionID)
} catch (error) {
await payload.db.rollbackTransaction(transactionID)
throw error
}
```
**Note**: MongoDB requires replicaset for transactions. SQLite requires `transactionOptions: {}` to enable.
### Threading req Through Operations
**Critical**: When performing nested operations in hooks, always pass `req` to maintain transaction context. Failing to do so breaks atomicity and can cause partial updates.
```ts
import type { CollectionAfterChangeHook } from 'payload'
// ✅ CORRECT: Thread req through nested operations
const resaveChildren: CollectionAfterChangeHook = async ({ collection, doc, req }) => {
// Find children - pass req
const children = await req.payload.find({
collection: 'children',
where: { parent: { equals: doc.id } },
req, // Maintains transaction context
})
// Update each child - pass req
for (const child of children.docs) {
await req.payload.update({
id: child.id,
collection: 'children',
data: { updatedField: 'value' },
req, // Same transaction as parent operation
})
}
}
// ❌ WRONG: Missing req breaks transaction
const brokenHook: CollectionAfterChangeHook = async ({ collection, doc, req }) => {
const children = await req.payload.find({
collection: 'children',
where: { parent: { equals: doc.id } },
// Missing req - separate transaction or no transaction
})
for (const child of children.docs) {
await req.payload.update({
id: child.id,
collection: 'children',
data: { updatedField: 'value' },
// Missing req - if parent operation fails, these updates persist
})
}
}
```
**Why This Matters:**
- **MongoDB (with replica sets)**: Creates atomic session across operations
- **PostgreSQL**: All operations use same Drizzle transaction
- **SQLite (with transactions enabled)**: Ensures rollback on errors
- **Without req**: Each operation runs independently, breaking atomicity
**When req is Required:**
- All mutating operations in hooks (create, update, delete)
- Operations that must succeed/fail together
- When using MongoDB replica sets or Postgres
- Any operation that relies on `req.context` or `req.user`
**When req is Optional:**
- Read-only lookups independent of current transaction
- Operations with `disableTransaction: true`
- Administrative operations with `overrideAccess: true`
## Storage Adapters
Available storage adapters:
- **@payloadcms/storage-s3** - AWS S3
- **@payloadcms/storage-azure** - Azure Blob Storage
- **@payloadcms/storage-gcs** - Google Cloud Storage
- **@payloadcms/storage-r2** - Cloudflare R2
- **@payloadcms/storage-vercel-blob** - Vercel Blob
- **@payloadcms/storage-uploadthing** - Uploadthing
### AWS S3
```ts
import { s3Storage } from '@payloadcms/storage-s3'
export default buildConfig({
plugins: [
s3Storage({
collections: {
media: true,
},
bucket: process.env.S3_BUCKET,
config: {
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
},
region: process.env.S3_REGION,
},
}),
],
})
```
### Azure Blob Storage
```ts
import { azureStorage } from '@payloadcms/storage-azure'
export default buildConfig({
plugins: [
azureStorage({
collections: {
media: true,
},
connectionString: process.env.AZURE_STORAGE_CONNECTION_STRING,
containerName: process.env.AZURE_STORAGE_CONTAINER_NAME,
}),
],
})
```
### Google Cloud Storage
```ts
import { gcsStorage } from '@payloadcms/storage-gcs'
export default buildConfig({
plugins: [
gcsStorage({
collections: {
media: true,
},
bucket: process.env.GCS_BUCKET,
options: {
projectId: process.env.GCS_PROJECT_ID,
credentials: JSON.parse(process.env.GCS_CREDENTIALS),
},
}),
],
})
```
### Cloudflare R2
```ts
import { r2Storage } from '@payloadcms/storage-r2'
export default buildConfig({
plugins: [
r2Storage({
collections: {
media: true,
},
bucket: process.env.R2_BUCKET,
config: {
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
},
region: 'auto',
endpoint: process.env.R2_ENDPOINT,
},
}),
],
})
```
### Vercel Blob
```ts
import { vercelBlobStorage } from '@payloadcms/storage-vercel-blob'
export default buildConfig({
plugins: [
vercelBlobStorage({
collections: {
media: true,
},
token: process.env.BLOB_READ_WRITE_TOKEN,
}),
],
})
```
### Uploadthing
```ts
import { uploadthingStorage } from '@payloadcms/storage-uploadthing'
export default buildConfig({
plugins: [
uploadthingStorage({
collections: {
media: true,
},
options: {
token: process.env.UPLOADTHING_TOKEN,
acl: 'public-read',
},
}),
],
})
```
## Email Adapters
### Nodemailer (SMTP)
```ts
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
export default buildConfig({
email: nodemailerAdapter({
defaultFromAddress: 'noreply@example.com',
defaultFromName: 'My App',
transportOptions: {
host: process.env.SMTP_HOST,
port: 587,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
},
}),
})
```
### Resend
```ts
import { resendAdapter } from '@payloadcms/email-resend'
export default buildConfig({
email: resendAdapter({
defaultFromAddress: 'noreply@example.com',
defaultFromName: 'My App',
apiKey: process.env.RESEND_API_KEY,
}),
})
```

View File

@@ -0,0 +1,386 @@
# Payload Advanced Features
Complete reference for authentication, jobs, custom endpoints, components, plugins, and localization.
## Authentication
### Login
```ts
// REST API
const response = await fetch('/api/users/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'user@example.com',
password: 'password',
}),
})
// Local API
const result = await payload.login({
collection: 'users',
data: {
email: 'user@example.com',
password: 'password',
},
})
```
### Forgot Password
```ts
await payload.forgotPassword({
collection: 'users',
data: {
email: 'user@example.com',
},
})
```
### Custom Strategy
```ts
import type { CollectionConfig, Strategy } from 'payload'
const customStrategy: Strategy = {
name: 'custom',
authenticate: async ({ payload, headers }) => {
const token = headers.get('authorization')?.split(' ')[1]
if (!token) return { user: null }
const user = await verifyToken(token)
return { user }
},
}
export const Users: CollectionConfig = {
slug: 'users',
auth: {
strategies: [customStrategy],
},
fields: [],
}
```
### API Keys
```ts
import type { CollectionConfig } from 'payload'
export const APIKeys: CollectionConfig = {
slug: 'api-keys',
auth: {
disableLocalStrategy: true,
useAPIKey: true,
},
fields: [],
}
```
## Jobs Queue
Offload long-running or scheduled tasks to background workers.
### Tasks
```ts
import { buildConfig } from 'payload'
import type { TaskConfig } from 'payload'
export default buildConfig({
jobs: {
tasks: [
{
slug: 'sendWelcomeEmail',
inputSchema: [
{ name: 'userEmail', type: 'text', required: true },
{ name: 'userName', type: 'text', required: true },
],
outputSchema: [{ name: 'emailSent', type: 'checkbox', required: true }],
retries: 2, // Retry up to 2 times on failure
handler: async ({ input, req }) => {
await sendEmail({
to: input.userEmail,
subject: `Welcome ${input.userName}`,
})
return { output: { emailSent: true } }
},
} as TaskConfig<'sendWelcomeEmail'>,
],
},
})
```
### Queueing Jobs
```ts
// In a hook or endpoint
await req.payload.jobs.queue({
task: 'sendWelcomeEmail',
input: {
userEmail: 'user@example.com',
userName: 'John',
},
waitUntil: new Date('2024-12-31'), // Optional: schedule for future
})
```
### Workflows
Multi-step jobs that run in sequence:
```ts
{
slug: 'onboardUser',
inputSchema: [{ name: 'userId', type: 'text' }],
handler: async ({ job, req }) => {
const results = await job.runInlineTask({
task: async ({ input }) => {
// Step 1: Send welcome email
await sendEmail(input.userId)
return { output: { emailSent: true } }
},
})
await job.runInlineTask({
task: async () => {
// Step 2: Create onboarding tasks
await createTasks()
return { output: { tasksCreated: true } }
},
})
},
}
```
## Custom Endpoints
Add custom REST API routes to collections, globals, or root config. See [ENDPOINTS.md](ENDPOINTS.md) for detailed patterns, authentication, helpers, and real-world examples.
### Root Endpoints
```ts
import { buildConfig } from 'payload'
import type { Endpoint } from 'payload'
const helloEndpoint: Endpoint = {
path: '/hello',
method: 'get',
handler: () => {
return Response.json({ message: 'Hello!' })
},
}
const greetEndpoint: Endpoint = {
path: '/greet/:name',
method: 'get',
handler: (req) => {
return Response.json({
message: `Hello ${req.routeParams.name}!`,
})
},
}
export default buildConfig({
endpoints: [helloEndpoint, greetEndpoint],
collections: [],
secret: process.env.PAYLOAD_SECRET || '',
})
```
### Collection Endpoints
```ts
import type { CollectionConfig, Endpoint } from 'payload'
const featuredEndpoint: Endpoint = {
path: '/featured',
method: 'get',
handler: async (req) => {
const posts = await req.payload.find({
collection: 'posts',
where: { featured: { equals: true } },
})
return Response.json(posts)
},
}
export const Posts: CollectionConfig = {
slug: 'posts',
endpoints: [featuredEndpoint],
fields: [
{ name: 'title', type: 'text' },
{ name: 'featured', type: 'checkbox' },
],
}
```
## Custom Components
### Field Component (Client)
```tsx
'use client'
import { useField } from '@payloadcms/ui'
import type { TextFieldClientComponent } from 'payload'
export const CustomField: TextFieldClientComponent = () => {
const { value, setValue } = useField()
return <input value={value || ''} onChange={(e) => setValue(e.target.value)} />
}
```
### Custom View
```tsx
'use client'
import { DefaultTemplate } from '@payloadcms/next/templates'
export const CustomView = () => {
return (
<DefaultTemplate>
<h1>Custom Dashboard</h1>
{/* Your content */}
</DefaultTemplate>
)
}
```
### Admin Config
```ts
import { buildConfig } from 'payload'
export default buildConfig({
admin: {
components: {
beforeDashboard: ['/components/BeforeDashboard'],
beforeLogin: ['/components/BeforeLogin'],
views: {
custom: {
Component: '/views/Custom',
path: '/custom',
},
},
},
},
collections: [],
secret: process.env.PAYLOAD_SECRET || '',
})
```
## Plugins
### Available Plugins
- **@payloadcms/plugin-seo** - SEO fields with meta title/description, Open Graph, preview generation
- **@payloadcms/plugin-redirects** - Manage URL redirects (301/302) for Next.js apps
- **@payloadcms/plugin-nested-docs** - Hierarchical document structures with breadcrumbs
- **@payloadcms/plugin-form-builder** - Dynamic form builder with submissions and validation
- **@payloadcms/plugin-search** - Full-text search integration (Algolia support)
- **@payloadcms/plugin-stripe** - Stripe payments, subscriptions, webhooks
- **@payloadcms/plugin-ecommerce** - Complete ecommerce solution (products, variants, carts, orders)
- **@payloadcms/plugin-import-export** - Import/export data via CSV
- **@payloadcms/plugin-multi-tenant** - Multi-tenancy with tenant isolation
- **@payloadcms/plugin-sentry** - Sentry error tracking integration
- **@payloadcms/plugin-mcp** - Model Context Protocol for AI integrations
### Using Plugins
```ts
import { buildConfig } from 'payload'
import { seoPlugin } from '@payloadcms/plugin-seo'
import { redirectsPlugin } from '@payloadcms/plugin-redirects'
export default buildConfig({
plugins: [
seoPlugin({
collections: ['posts', 'pages'],
}),
redirectsPlugin({
collections: ['pages'],
}),
],
collections: [],
secret: process.env.PAYLOAD_SECRET || '',
})
```
### Creating Plugins
```ts
import type { Config } from 'payload'
interface PluginOptions {
enabled?: boolean
}
export const myPlugin =
(options: PluginOptions) =>
(config: Config): Config => ({
...config,
collections: [
...(config.collections || []),
{
slug: 'plugin-collection',
fields: [{ name: 'title', type: 'text' }],
},
],
onInit: async (payload) => {
if (config.onInit) await config.onInit(payload)
// Plugin initialization
},
})
```
## Localization
```ts
import { buildConfig } from 'payload'
import type { Field, Payload } from 'payload'
export default buildConfig({
localization: {
locales: ['en', 'es', 'de'],
defaultLocale: 'en',
fallback: true,
},
collections: [],
secret: process.env.PAYLOAD_SECRET || '',
})
// Localized field
const localizedField: TextField = {
name: 'title',
type: 'text',
localized: true,
}
// Query with locale
const posts = await payload.find({
collection: 'posts',
locale: 'es',
})
```
## TypeScript Type References
For complete TypeScript type definitions and signatures, reference these files from the Payload source:
### Core Configuration Types
- **[All Commonly-Used Types](https://github.com/payloadcms/payload/blob/main/packages/payload/src/index.ts)** - Check here first for commonly used types and interfaces. All core types are exported from this file.
### Database & Adapters
- **[Database Adapter Types](https://github.com/payloadcms/payload/blob/main/packages/payload/src/database/types.ts)** - Base adapter interface
- **[MongoDB Adapter](https://github.com/payloadcms/payload/blob/main/packages/db-mongodb/src/index.ts)** - MongoDB-specific options
- **[Postgres Adapter](https://github.com/payloadcms/payload/blob/main/packages/db-postgres/src/index.ts)** - Postgres-specific options
### Rich Text & Plugins
- **[Lexical Types](https://github.com/payloadcms/payload/blob/main/packages/richtext-lexical/src/exports/server/index.ts)** - Lexical editor configuration
When users need detailed type information, fetch these URLs to provide complete signatures and optional parameters.

View File

@@ -0,0 +1,303 @@
# Payload Collections Reference
Complete reference for collection configurations and patterns.
## Basic Collection
```ts
import type { CollectionConfig } from 'payload'
export const Posts: CollectionConfig = {
slug: 'posts',
labels: {
singular: 'Post',
plural: 'Posts',
},
admin: {
useAsTitle: 'title',
defaultColumns: ['title', 'author', 'status', 'createdAt'],
group: 'Content', // Organize in admin sidebar
description: 'Blog posts and articles',
listSearchableFields: ['title', 'slug'],
},
fields: [
{
name: 'title',
type: 'text',
required: true,
index: true,
},
{
name: 'slug',
type: 'text',
unique: true,
index: true,
admin: { position: 'sidebar' },
},
{
name: 'status',
type: 'select',
options: ['draft', 'published'],
defaultValue: 'draft',
},
],
defaultSort: '-createdAt',
timestamps: true,
}
```
## Auth Collection
```ts
export const Users: CollectionConfig = {
slug: 'users',
auth: {
tokenExpiration: 7200, // 2 hours
verify: true,
maxLoginAttempts: 5,
lockTime: 600000, // 10 minutes
useAPIKey: true,
},
admin: {
useAsTitle: 'email',
},
fields: [
{
name: 'roles',
type: 'select',
hasMany: true,
options: ['admin', 'editor', 'user'],
required: true,
defaultValue: ['user'],
saveToJWT: true,
},
{
name: 'name',
type: 'text',
required: true,
},
],
}
```
## Upload Collection
```ts
export const Media: CollectionConfig = {
slug: 'media',
upload: {
staticDir: 'media',
mimeTypes: ['image/*'],
imageSizes: [
{
name: 'thumbnail',
width: 400,
height: 300,
position: 'centre',
},
{
name: 'card',
width: 768,
height: 1024,
},
],
adminThumbnail: 'thumbnail',
focalPoint: true,
crop: true,
},
access: {
read: () => true,
},
fields: [
{
name: 'alt',
type: 'text',
required: true,
},
{
name: 'caption',
type: 'text',
localized: true,
},
],
}
```
## Live Preview
Enable real-time content preview during editing.
```ts
import type { CollectionConfig } from 'payload'
const generatePreviewPath = ({
slug,
collection,
req,
}: {
slug: string
collection: string
req: any
}) => {
const baseUrl = process.env.NEXT_PUBLIC_SERVER_URL
return `${baseUrl}/api/preview?slug=${slug}&collection=${collection}`
}
export const Pages: CollectionConfig = {
slug: 'pages',
admin: {
useAsTitle: 'title',
// Live preview during editing
livePreview: {
url: ({ data, req }) =>
generatePreviewPath({
slug: data?.slug as string,
collection: 'pages',
req,
}),
},
// Static preview button
preview: (data, { req }) =>
generatePreviewPath({
slug: data?.slug as string,
collection: 'pages',
req,
}),
},
fields: [
{ name: 'title', type: 'text' },
{ name: 'slug', type: 'text' },
],
}
```
## Versioning & Drafts
Payload maintains version history and supports draft/publish workflows.
```ts
import type { CollectionConfig } from 'payload'
// Basic versioning (audit log only)
export const Users: CollectionConfig = {
slug: 'users',
versions: true, // or { maxPerDoc: 100 }
fields: [{ name: 'name', type: 'text' }],
}
// Drafts enabled (draft/publish workflow)
export const Posts: CollectionConfig = {
slug: 'posts',
versions: {
drafts: true, // Enables _status field
maxPerDoc: 50,
},
fields: [{ name: 'title', type: 'text' }],
}
// Full configuration with autosave and scheduled publish
export const Pages: CollectionConfig = {
slug: 'pages',
versions: {
drafts: {
autosave: true, // Auto-save while editing
schedulePublish: true, // Schedule future publish/unpublish
validate: false, // Don't validate drafts (default)
},
maxPerDoc: 100, // Keep last 100 versions (0 = unlimited)
},
fields: [{ name: 'title', type: 'text' }],
}
```
### Draft API Usage
```ts
// Create draft
await payload.create({
collection: 'posts',
data: { title: 'Draft Post' },
draft: true, // Saves as draft, skips required field validation
})
// Update as draft
await payload.update({
collection: 'posts',
id: '123',
data: { title: 'Updated Draft' },
draft: true,
})
// Read with drafts (returns newest draft if available)
const post = await payload.findByID({
collection: 'posts',
id: '123',
draft: true, // Returns draft version if exists
})
// Query only published (REST API)
// GET /api/posts (returns only _status: 'published')
// Access control for drafts
export const Posts: CollectionConfig = {
slug: 'posts',
versions: { drafts: true },
access: {
read: ({ req: { user } }) => {
// Public can only see published
if (!user) return { _status: { equals: 'published' } }
// Authenticated can see all
return true
},
},
fields: [{ name: 'title', type: 'text' }],
}
```
### Document Status
The `_status` field is auto-injected when drafts are enabled:
- `draft` - Never published
- `published` - Published with no newer drafts
- `changed` - Published but has newer unpublished drafts
## Globals
Globals are single-instance documents (not collections).
```ts
import type { GlobalConfig } from 'payload'
export const Header: GlobalConfig = {
slug: 'header',
label: 'Header',
admin: {
group: 'Settings',
},
fields: [
{
name: 'logo',
type: 'upload',
relationTo: 'media',
required: true,
},
{
name: 'nav',
type: 'array',
maxRows: 8,
fields: [
{
name: 'link',
type: 'relationship',
relationTo: 'pages',
},
{
name: 'label',
type: 'text',
},
],
},
],
}
```

View File

@@ -0,0 +1,634 @@
# Payload Custom API Endpoints Reference
Custom REST API endpoints extend Payload's auto-generated CRUD operations with custom logic, authentication flows, webhooks, and integrations.
## Quick Reference
### Endpoint Configuration
| Property | Type | Description |
| --------- | ------------------------------------------------- | --------------------------------------------------------------- |
| `path` | `string` | Route path after collection/global slug (e.g., `/:id/tracking`) |
| `method` | `'get' \| 'post' \| 'put' \| 'patch' \| 'delete'` | HTTP method (lowercase) |
| `handler` | `(req: PayloadRequest) => Promise<Response>` | Async function returning Web API Response |
| `custom` | `Record<string, any>` | Extension point for plugins/metadata |
### Request Context
| Property | Type | Description |
| ----------------- | ----------------------- | ------------------------------------------------------ |
| `req.user` | `User \| null` | Authenticated user (null if not authenticated) |
| `req.payload` | `Payload` | Payload instance for operations (find, create...) |
| `req.routeParams` | `Record<string, any>` | Path parameters (e.g., `:id`) |
| `req.url` | `string` | Full request URL |
| `req.method` | `string` | HTTP method |
| `req.headers` | `Headers` | Request headers |
| `req.json()` | `() => Promise<any>` | Parse JSON body |
| `req.text()` | `() => Promise<string>` | Read body as text |
| `req.data` | `any` | Parsed body (after `addDataAndFileToRequest()`) |
| `req.file` | `File` | Uploaded file (after `addDataAndFileToRequest()`) |
| `req.locale` | `string` | Request locale (after `addLocalesToRequestFromData()`) |
| `req.i18n` | `I18n` | i18n instance |
| `req.t` | `TFunction` | Translation function |
## Common Patterns
### Authentication Check
Custom endpoints are **not authenticated by default**. Check `req.user` to enforce authentication.
```ts
import { APIError } from 'payload'
export const authenticatedEndpoint = {
path: '/protected',
method: 'get',
handler: async (req) => {
if (!req.user) {
throw new APIError('Unauthorized', 401)
}
// User is authenticated
return Response.json({ message: 'Access granted' })
},
}
```
### Using Payload Operations
Use `req.payload` for database operations with access control and hooks.
```ts
export const getRelatedPosts = {
path: '/:id/related',
method: 'get',
handler: async (req) => {
const { id } = req.routeParams
// Find related posts
const posts = await req.payload.find({
collection: 'posts',
where: {
category: {
equals: id,
},
},
limit: 5,
sort: '-createdAt',
})
return Response.json(posts)
},
}
```
### Route Parameters
Access path parameters via `req.routeParams`.
```ts
export const getTrackingEndpoint = {
path: '/:id/tracking',
method: 'get',
handler: async (req) => {
const orderId = req.routeParams.id
const tracking = await getTrackingInfo(orderId)
if (!tracking) {
return Response.json({ error: 'not found' }, { status: 404 })
}
return Response.json(tracking)
},
}
```
### Request Body Handling
**Option 1: Manual JSON parsing**
```ts
export const createEndpoint = {
path: '/create',
method: 'post',
handler: async (req) => {
const data = await req.json()
const result = await req.payload.create({
collection: 'posts',
data,
})
return Response.json(result)
},
}
```
**Option 2: Using helper (handles JSON + files)**
```ts
import { addDataAndFileToRequest } from 'payload'
export const uploadEndpoint = {
path: '/upload',
method: 'post',
handler: async (req) => {
await addDataAndFileToRequest(req)
// req.data now contains parsed body
// req.file contains uploaded file (if multipart)
const result = await req.payload.create({
collection: 'media',
data: req.data,
file: req.file,
})
return Response.json(result)
},
}
```
### CORS Headers
Use `headersWithCors` helper to apply config CORS settings.
```ts
import { headersWithCors } from 'payload'
export const corsEndpoint = {
path: '/public-data',
method: 'get',
handler: async (req) => {
const data = await fetchPublicData()
return Response.json(data, {
headers: headersWithCors({
headers: new Headers(),
req,
}),
})
},
}
```
### Error Handling
Throw `APIError` with status codes for proper error responses.
```ts
import { APIError } from 'payload'
export const validateEndpoint = {
path: '/validate',
method: 'post',
handler: async (req) => {
const data = await req.json()
if (!data.email) {
throw new APIError('Email is required', 400)
}
// Validation passed
return Response.json({ valid: true })
},
}
```
### Query Parameters
Extract query params from URL.
```ts
export const searchEndpoint = {
path: '/search',
method: 'get',
handler: async (req) => {
const url = new URL(req.url)
const query = url.searchParams.get('q')
const limit = parseInt(url.searchParams.get('limit') || '10')
const results = await req.payload.find({
collection: 'posts',
where: {
title: {
contains: query,
},
},
limit,
})
return Response.json(results)
},
}
```
## Helper Functions
### addDataAndFileToRequest
Parses request body and attaches to `req.data` and `req.file`.
```ts
import { addDataAndFileToRequest } from 'payload'
export const endpoint = {
path: '/process',
method: 'post',
handler: async (req) => {
await addDataAndFileToRequest(req)
// req.data: parsed JSON or form data
// req.file: uploaded file (if multipart)
console.log(req.data) // { title: 'My Post' }
console.log(req.file) // File object or undefined
},
}
```
**Handles:**
- JSON bodies (`Content-Type: application/json`)
- Form data (`Content-Type: multipart/form-data`)
- File uploads
### addLocalesToRequestFromData
Extracts locale from request data and validates against config.
```ts
import { addLocalesToRequestFromData } from 'payload'
export const endpoint = {
path: '/translate',
method: 'post',
handler: async (req) => {
await addLocalesToRequestFromData(req)
// req.locale: validated locale string
// req.fallbackLocale: fallback locale string
const result = await req.payload.find({
collection: 'posts',
locale: req.locale,
})
return Response.json(result)
},
}
```
### headersWithCors
Applies CORS headers from Payload config.
```ts
import { headersWithCors } from 'payload'
export const endpoint = {
path: '/data',
method: 'get',
handler: async (req) => {
const data = { message: 'Hello' }
return Response.json(data, {
headers: headersWithCors({
headers: new Headers({
'Cache-Control': 'public, max-age=3600',
}),
req,
}),
})
},
}
```
## Real-World Examples
### Multi-Tenant Login Endpoint
From `examples/multi-tenant`:
```ts
import { APIError, generatePayloadCookie, headersWithCors } from 'payload'
export const externalUsersLogin = {
path: '/login-external',
method: 'post',
handler: async (req) => {
const { email, password, tenant } = await req.json()
if (!email || !password || !tenant) {
throw new APIError('Missing credentials', 400)
}
// Find user with tenant constraint
const userQuery = await req.payload.find({
collection: 'users',
where: {
and: [
{ email: { equals: email } },
{
or: [{ tenants: { equals: tenant } }, { 'tenants.tenant': { equals: tenant } }],
},
],
},
})
if (!userQuery.docs.length) {
throw new APIError('Invalid credentials', 401)
}
// Authenticate user
const result = await req.payload.login({
collection: 'users',
data: { email, password },
})
return Response.json(result, {
headers: headersWithCors({
headers: new Headers({
'Set-Cookie': generatePayloadCookie({
collectionAuthConfig: req.payload.config.collections.find((c) => c.slug === 'users')
.auth,
cookiePrefix: req.payload.config.cookiePrefix,
token: result.token,
}),
}),
req,
}),
})
},
}
```
### Webhook Handler (Stripe)
From `packages/plugin-ecommerce`:
```ts
export const webhookEndpoint = {
path: '/webhooks',
method: 'post',
handler: async (req) => {
const body = await req.text()
const signature = req.headers.get('stripe-signature')
try {
const event = stripe.webhooks.constructEvent(body, signature, webhookSecret)
// Process event
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSuccess(req.payload, event.data.object)
break
case 'payment_intent.failed':
await handlePaymentFailure(req.payload, event.data.object)
break
}
return Response.json({ received: true })
} catch (err) {
req.payload.logger.error(`Webhook error: ${err.message}`)
return Response.json({ error: err.message }, { status: 400 })
}
},
}
```
### Data Preview Endpoint
From `packages/plugin-import-export`:
```ts
import { addDataAndFileToRequest } from 'payload'
export const previewEndpoint = {
path: '/preview',
method: 'post',
handler: async (req) => {
if (!req.user) {
throw new APIError('Unauthorized', 401)
}
await addDataAndFileToRequest(req)
const { collection, where, limit = 10 } = req.data
// Validate collection exists
const collectionConfig = req.payload.config.collections.find((c) => c.slug === collection)
if (!collectionConfig) {
throw new APIError('Collection not found', 404)
}
// Preview data
const results = await req.payload.find({
collection,
where,
limit,
depth: 0,
})
return Response.json({
docs: results.docs,
totalDocs: results.totalDocs,
fields: collectionConfig.fields,
})
},
}
```
### Reindex Action Endpoint
From `packages/plugin-search`:
```ts
export const reindexEndpoint = (pluginConfig) => ({
path: '/reindex',
method: 'post',
handler: async (req) => {
if (!req.user) {
throw new APIError('Unauthorized', 401)
}
const { collection } = req.routeParams
// Reindex collection
const result = await reindexCollection(req.payload, collection, pluginConfig)
return Response.json({
message: `Reindexed ${result.count} documents`,
count: result.count,
})
},
})
```
## Endpoint Placement
### Collection Endpoints
Mounted at `/api/{collection-slug}/{path}`.
```ts
import type { CollectionConfig } from 'payload'
export const Orders: CollectionConfig = {
slug: 'orders',
fields: [
/* ... */
],
endpoints: [
{
path: '/:id/tracking',
method: 'get',
handler: async (req) => {
// Available at: /api/orders/:id/tracking
const orderId = req.routeParams.id
return Response.json({ orderId })
},
},
],
}
```
### Global Endpoints
Mounted at `/api/globals/{global-slug}/{path}`.
```ts
import type { GlobalConfig } from 'payload'
export const Settings: GlobalConfig = {
slug: 'settings',
fields: [
/* ... */
],
endpoints: [
{
path: '/clear-cache',
method: 'post',
handler: async (req) => {
// Available at: /api/globals/settings/clear-cache
await clearCache()
return Response.json({ message: 'Cache cleared' })
},
},
],
}
```
## Advanced Patterns
### Factory Functions
Create reusable endpoint factories for plugins.
```ts
export const createWebhookEndpoint = (config) => ({
path: '/webhook',
method: 'post',
handler: async (req) => {
const signature = req.headers.get('x-webhook-signature')
if (!verifySignature(signature, config.secret)) {
throw new APIError('Invalid signature', 401)
}
const data = await req.json()
await processWebhook(req.payload, data, config)
return Response.json({ received: true })
},
})
```
### Conditional Endpoints
Add endpoints based on config options.
```ts
export const MyCollection: CollectionConfig = {
slug: 'posts',
fields: [
/* ... */
],
endpoints: [
// Always included
{
path: '/public',
method: 'get',
handler: async (req) => Response.json({ data: [] }),
},
// Conditionally included
...(process.env.ENABLE_ANALYTICS
? [
{
path: '/analytics',
method: 'get',
handler: async (req) => Response.json({ analytics: [] }),
},
]
: []),
],
}
```
### OpenAPI Documentation
Use `custom` property for API documentation metadata.
```ts
export const endpoint = {
path: '/search',
method: 'get',
handler: async (req) => {
// Handler implementation
},
custom: {
openapi: {
summary: 'Search posts',
parameters: [
{
name: 'q',
in: 'query',
required: true,
schema: { type: 'string' },
},
],
responses: {
200: {
description: 'Search results',
content: {
'application/json': {
schema: { type: 'array' },
},
},
},
},
},
},
}
```
## Best Practices
1. **Always check authentication** - Custom endpoints are not authenticated by default
2. **Use `req.payload` for operations** - Ensures access control and hooks execute
3. **Use helpers for common tasks** - `addDataAndFileToRequest`, `headersWithCors`, etc.
4. **Throw `APIError` for errors** - Provides consistent error responses
5. **Return Web API `Response`** - Use `Response.json()` for consistent responses
6. **Validate input** - Check required fields, validate types
7. **Handle CORS** - Use `headersWithCors` for cross-origin requests
8. **Log errors** - Use `req.payload.logger` for debugging
9. **Document with `custom`** - Add OpenAPI metadata for API docs
10. **Factory pattern for reuse** - Create endpoint factories for plugins
## Resources
- REST API Overview: <https://payloadcms.com/docs/rest-api/overview>
- Custom Endpoints: <https://payloadcms.com/docs/rest-api/overview#custom-endpoints>
- Access Control: <https://payloadcms.com/docs/access-control/overview>
- Local API: <https://payloadcms.com/docs/local-api/overview>

View File

@@ -0,0 +1,553 @@
# Payload Field Type Guards Reference
Complete reference with detailed examples and patterns. See [FIELDS.md](FIELDS.md#field-type-guards) for quick reference table of all guards.
## Structural Guards
### fieldHasSubFields
Checks if field contains nested fields (group, array, row, or collapsible).
```ts
import type { Field } from 'payload'
import { fieldHasSubFields } from 'payload'
function traverseFields(fields: Field[]): void {
fields.forEach((field) => {
if (fieldHasSubFields(field)) {
// Safe to access field.fields
traverseFields(field.fields)
}
})
}
```
**Signature:**
```ts
fieldHasSubFields<TField extends ClientField | Field>(
field: TField
): field is TField & (FieldWithSubFieldsClient | FieldWithSubFields)
```
**Common Pattern - Exclude Arrays:**
```ts
if (fieldHasSubFields(field) && !fieldIsArrayType(field)) {
// Groups, rows, collapsibles only (not arrays)
}
```
### fieldIsArrayType
Checks if field type is `'array'`.
```ts
import { fieldIsArrayType } from 'payload'
if (fieldIsArrayType(field)) {
// field.type === 'array'
console.log(`Min rows: ${field.minRows}`)
console.log(`Max rows: ${field.maxRows}`)
}
```
**Signature:**
```ts
fieldIsArrayType<TField extends ClientField | Field>(
field: TField
): field is TField & (ArrayFieldClient | ArrayField)
```
### fieldIsBlockType
Checks if field type is `'blocks'`.
```ts
import { fieldIsBlockType } from 'payload'
if (fieldIsBlockType(field)) {
// field.type === 'blocks'
field.blocks.forEach((block) => {
console.log(`Block: ${block.slug}`)
})
}
```
**Signature:**
```ts
fieldIsBlockType<TField extends ClientField | Field>(
field: TField
): field is TField & (BlocksFieldClient | BlocksField)
```
**Common Pattern - Distinguish Containers:**
```ts
if (fieldIsArrayType(field)) {
// Handle array rows
} else if (fieldIsBlockType(field)) {
// Handle block types
}
```
### fieldIsGroupType
Checks if field type is `'group'`.
```ts
import { fieldIsGroupType } from 'payload'
if (fieldIsGroupType(field)) {
// field.type === 'group'
console.log(`Interface: ${field.interfaceName}`)
}
```
**Signature:**
```ts
fieldIsGroupType<TField extends ClientField | Field>(
field: TField
): field is TField & (GroupFieldClient | GroupField)
```
## Capability Guards
### fieldSupportsMany
Checks if field can have multiple values (select, relationship, or upload with `hasMany`).
```ts
import { fieldSupportsMany } from 'payload'
if (fieldSupportsMany(field)) {
// field.type is 'select' | 'relationship' | 'upload'
// Safe to check field.hasMany
if (field.hasMany) {
console.log('Field accepts multiple values')
}
}
```
**Signature:**
```ts
fieldSupportsMany<TField extends ClientField | Field>(
field: TField
): field is TField & (FieldWithManyClient | FieldWithMany)
```
### fieldHasMaxDepth
Checks if field is relationship/upload/join with numeric `maxDepth` property.
```ts
import { fieldHasMaxDepth } from 'payload'
if (fieldHasMaxDepth(field)) {
// field.type is 'upload' | 'relationship' | 'join'
// AND field.maxDepth is number
const remainingDepth = field.maxDepth - currentDepth
}
```
**Signature:**
```ts
fieldHasMaxDepth<TField extends ClientField | Field>(
field: TField
): field is TField & (FieldWithMaxDepthClient | FieldWithMaxDepth)
```
### fieldShouldBeLocalized
Checks if field needs localization handling (accounts for parent localization).
```ts
import { fieldShouldBeLocalized } from 'payload'
function processField(field: Field, parentIsLocalized: boolean) {
if (fieldShouldBeLocalized({ field, parentIsLocalized })) {
// Create locale-specific table or index
}
}
```
**Signature:**
```ts
fieldShouldBeLocalized({
field,
parentIsLocalized,
}: {
field: ClientField | ClientTab | Field | Tab
parentIsLocalized: boolean
}): boolean
```
```ts
// Accounts for parent localization
if (fieldShouldBeLocalized({ field, parentIsLocalized: false })) {
/* ... */
}
```
### fieldIsVirtual
Checks if field is virtual (computed or virtual relationship).
```ts
import { fieldIsVirtual } from 'payload'
if (fieldIsVirtual(field)) {
// field.virtual is truthy
if (typeof field.virtual === 'string') {
// Virtual relationship path
console.log(`Virtual path: ${field.virtual}`)
} else {
// Computed virtual field (uses hooks)
}
}
```
**Signature:**
```ts
fieldIsVirtual(field: Field | Tab): boolean
```
## Data Guards
### fieldAffectsData
**Most commonly used guard.** Checks if field stores data (has name and is not UI-only).
```ts
import { fieldAffectsData } from 'payload'
function generateSchema(fields: Field[]) {
fields.forEach((field) => {
if (fieldAffectsData(field)) {
// Safe to access field.name
schema[field.name] = getFieldType(field)
}
})
}
```
**Signature:**
```ts
fieldAffectsData<TField extends ClientField | Field | TabAsField | TabAsFieldClient>(
field: TField
): field is TField & (FieldAffectingDataClient | FieldAffectingData)
```
**Pattern - Data Fields Only:**
```ts
const dataFields = fields.filter(fieldAffectsData)
```
### fieldIsPresentationalOnly
Checks if field is UI-only (type `'ui'`).
```ts
import { fieldIsPresentationalOnly } from 'payload'
if (fieldIsPresentationalOnly(field)) {
// field.type === 'ui'
// Skip in data operations, GraphQL schema, etc.
return
}
```
**Signature:**
```ts
fieldIsPresentationalOnly<TField extends ClientField | Field | TabAsField | TabAsFieldClient>(
field: TField
): field is TField & (UIFieldClient | UIField)
```
### fieldIsID
Checks if field name is exactly `'id'`.
```ts
import { fieldIsID } from 'payload'
if (fieldIsID(field)) {
// field.name === 'id'
// Special handling for ID field
}
```
**Signature:**
```ts
fieldIsID<TField extends ClientField | Field>(
field: TField
): field is { name: 'id' } & TField
```
### fieldIsHiddenOrDisabled
Checks if field is hidden or admin-disabled.
```ts
import { fieldIsHiddenOrDisabled } from 'payload'
const visibleFields = fields.filter((field) => !fieldIsHiddenOrDisabled(field))
```
**Signature:**
```ts
fieldIsHiddenOrDisabled<TField extends ClientField | Field | TabAsField | TabAsFieldClient>(
field: TField
): field is { admin: { hidden: true } } & TField
```
## Layout Guards
### fieldIsSidebar
Checks if field is positioned in sidebar.
```ts
import { fieldIsSidebar } from 'payload'
const [mainFields, sidebarFields] = fields.reduce(
([main, sidebar], field) => {
if (fieldIsSidebar(field)) {
return [main, [...sidebar, field]]
}
return [[...main, field], sidebar]
},
[[], []],
)
```
**Signature:**
```ts
fieldIsSidebar<TField extends ClientField | Field | TabAsField | TabAsFieldClient>(
field: TField
): field is { admin: { position: 'sidebar' } } & TField
```
## Tab & Group Guards
### tabHasName
Checks if tab is named (stores data under tab name).
```ts
import { tabHasName } from 'payload'
tabs.forEach((tab) => {
if (tabHasName(tab)) {
// tab.name exists
dataPath.push(tab.name)
}
// Process tab.fields
})
```
**Signature:**
```ts
tabHasName<TField extends ClientTab | Tab>(
tab: TField
): tab is NamedTab & TField
```
### groupHasName
Checks if group is named (stores data under group name).
```ts
import { groupHasName } from 'payload'
if (groupHasName(group)) {
// group.name exists
return data[group.name]
}
```
**Signature:**
```ts
groupHasName(group: Partial<NamedGroupFieldClient>): group is NamedGroupFieldClient
```
## Option & Value Guards
### optionIsObject
Checks if option is object format `{label, value}` vs string.
```ts
import { optionIsObject } from 'payload'
field.options.forEach((option) => {
if (optionIsObject(option)) {
console.log(`${option.label}: ${option.value}`)
} else {
console.log(option) // string value
}
})
```
**Signature:**
```ts
optionIsObject(option: Option): option is OptionObject
```
### optionsAreObjects
Checks if entire options array contains objects.
```ts
import { optionsAreObjects } from 'payload'
if (optionsAreObjects(field.options)) {
// All options are OptionObject[]
const labels = field.options.map((opt) => opt.label)
}
```
**Signature:**
```ts
optionsAreObjects(options: Option[]): options is OptionObject[]
```
### optionIsValue
Checks if option is string value (not object).
```ts
import { optionIsValue } from 'payload'
if (optionIsValue(option)) {
// option is string
const value = option
}
```
**Signature:**
```ts
optionIsValue(option: Option): option is string
```
### valueIsValueWithRelation
Checks if relationship value is polymorphic format `{relationTo, value}`.
```ts
import { valueIsValueWithRelation } from 'payload'
if (valueIsValueWithRelation(fieldValue)) {
// fieldValue.relationTo exists
// fieldValue.value exists
console.log(`Related to ${fieldValue.relationTo}: ${fieldValue.value}`)
}
```
**Signature:**
```ts
valueIsValueWithRelation(value: unknown): value is ValueWithRelation
```
## Common Patterns
### Recursive Field Traversal
```ts
import { fieldAffectsData, fieldHasSubFields } from 'payload'
function traverseFields(fields: Field[], callback: (field: Field) => void) {
fields.forEach((field) => {
if (fieldAffectsData(field)) {
callback(field)
}
if (fieldHasSubFields(field)) {
traverseFields(field.fields, callback)
}
})
}
```
### Filter Data-Bearing Fields
```ts
import { fieldAffectsData, fieldIsPresentationalOnly, fieldIsHiddenOrDisabled } from 'payload'
const dataFields = fields.filter(
(field) =>
fieldAffectsData(field) && !fieldIsPresentationalOnly(field) && !fieldIsHiddenOrDisabled(field),
)
```
### Container Type Switching
```ts
import { fieldIsArrayType, fieldIsBlockType, fieldHasSubFields } from 'payload'
if (fieldIsArrayType(field)) {
// Handle array-specific logic
} else if (fieldIsBlockType(field)) {
// Handle blocks-specific logic
} else if (fieldHasSubFields(field)) {
// Handle group/row/collapsible
}
```
### Safe Property Access
```ts
import { fieldSupportsMany, fieldHasMaxDepth } from 'payload'
// Without guard - TypeScript error
// if (field.hasMany) { /* ... */ }
// With guard - safe access
if (fieldSupportsMany(field) && field.hasMany) {
console.log('Multiple values supported')
}
if (fieldHasMaxDepth(field)) {
const depth = field.maxDepth // TypeScript knows this is number
}
```
## Type Preservation
All guards preserve the original type constraint:
```ts
import type { ClientField, Field } from 'payload'
import { fieldHasSubFields } from 'payload'
function processServerField(field: Field) {
if (fieldHasSubFields(field)) {
// field is Field & FieldWithSubFields (not ClientField)
}
}
function processClientField(field: ClientField) {
if (fieldHasSubFields(field)) {
// field is ClientField & FieldWithSubFieldsClient
}
}
```

View File

@@ -0,0 +1,744 @@
# Payload Field Types Reference
Complete reference for all Payload field types with examples.
## Text Field
```ts
import type { TextField } from 'payload'
const textField: TextField = {
name: 'title',
type: 'text',
required: true,
unique: true,
minLength: 5,
maxLength: 100,
index: true,
localized: true,
defaultValue: 'Default Title',
validate: (value) => Boolean(value) || 'Required',
admin: {
placeholder: 'Enter title...',
position: 'sidebar',
condition: (data) => data.showTitle === true,
},
}
```
### Slug Field Helper
Built-in helper for auto-generating slugs:
```ts
import { slugField } from 'payload'
import type { CollectionConfig } from 'payload'
export const Pages: CollectionConfig = {
slug: 'pages',
fields: [
{ name: 'title', type: 'text', required: true },
slugField({
name: 'slug', // defaults to 'slug'
useAsSlug: 'title', // defaults to 'title'
checkboxName: 'generateSlug', // defaults to 'generateSlug'
localized: true,
required: true,
overrides: (defaultField) => {
// Customize the generated fields if needed
return defaultField
},
}),
],
}
```
## Rich Text (Lexical)
```ts
import type { RichTextField } from 'payload'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import { HeadingFeature, LinkFeature } from '@payloadcms/richtext-lexical'
const richTextField: RichTextField = {
name: 'content',
type: 'richText',
required: true,
localized: true,
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
HeadingFeature({
enabledHeadingSizes: ['h1', 'h2', 'h3'],
}),
LinkFeature({
enabledCollections: ['posts', 'pages'],
}),
],
}),
}
```
### Advanced Lexical Configuration
```ts
import {
BoldFeature,
EXPERIMENTAL_TableFeature,
FixedToolbarFeature,
HeadingFeature,
IndentFeature,
InlineToolbarFeature,
ItalicFeature,
LinkFeature,
OrderedListFeature,
UnderlineFeature,
UnorderedListFeature,
lexicalEditor,
} from '@payloadcms/richtext-lexical'
// Global editor config with full features
export default buildConfig({
editor: lexicalEditor({
features: () => {
return [
UnderlineFeature(),
BoldFeature(),
ItalicFeature(),
OrderedListFeature(),
UnorderedListFeature(),
LinkFeature({
enabledCollections: ['pages'],
fields: ({ defaultFields }) => {
const defaultFieldsWithoutUrl = defaultFields.filter((field) => {
if ('name' in field && field.name === 'url') return false
return true
})
return [
...defaultFieldsWithoutUrl,
{
name: 'url',
type: 'text',
admin: {
condition: ({ linkType }) => linkType !== 'internal',
},
label: ({ t }) => t('fields:enterURL'),
required: true,
},
]
},
}),
IndentFeature(),
EXPERIMENTAL_TableFeature(),
]
},
}),
})
// Field-specific editor with custom toolbar
const richTextWithToolbars: RichTextField = {
name: 'richText',
type: 'richText',
editor: lexicalEditor({
features: ({ rootFeatures }) => {
return [
...rootFeatures,
HeadingFeature({ enabledHeadingSizes: ['h2', 'h3', 'h4'] }),
FixedToolbarFeature(),
InlineToolbarFeature(),
]
},
}),
label: false,
}
```
## Relationship
```ts
import type { RelationshipField } from 'payload'
// Single relationship
const singleRelationship: RelationshipField = {
name: 'author',
type: 'relationship',
relationTo: 'users',
required: true,
maxDepth: 2,
}
// Multiple relationships (hasMany)
const multipleRelationship: RelationshipField = {
name: 'categories',
type: 'relationship',
relationTo: 'categories',
hasMany: true,
filterOptions: {
active: { equals: true },
},
}
// Polymorphic relationship
const polymorphicRelationship: PolymorphicRelationshipField = {
name: 'relatedContent',
type: 'relationship',
relationTo: ['posts', 'pages'],
hasMany: true,
}
```
## Array
```ts
import type { ArrayField } from 'payload'
const arrayField: ArrayField = {
name: 'slides',
type: 'array',
minRows: 2,
maxRows: 10,
labels: {
singular: 'Slide',
plural: 'Slides',
},
fields: [
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'image',
type: 'upload',
relationTo: 'media',
},
],
admin: {
initCollapsed: true,
},
}
```
## Blocks
```ts
import type { BlocksField, Block } from 'payload'
const HeroBlock: Block = {
slug: 'hero',
interfaceName: 'HeroBlock',
fields: [
{
name: 'heading',
type: 'text',
required: true,
},
{
name: 'background',
type: 'upload',
relationTo: 'media',
},
],
}
const ContentBlock: Block = {
slug: 'content',
fields: [
{
name: 'text',
type: 'richText',
},
],
}
const blocksField: BlocksField = {
name: 'layout',
type: 'blocks',
blocks: [HeroBlock, ContentBlock],
}
```
## Select
```ts
import type { SelectField } from 'payload'
const selectField: SelectField = {
name: 'status',
type: 'select',
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
],
defaultValue: 'draft',
required: true,
}
// Multiple select
const multiSelectField: SelectField = {
name: 'tags',
type: 'select',
hasMany: true,
options: ['tech', 'news', 'sports'],
}
```
## Upload
```ts
import type { UploadField } from 'payload'
const uploadField: UploadField = {
name: 'featuredImage',
type: 'upload',
relationTo: 'media',
required: true,
filterOptions: {
mimeType: { contains: 'image' },
},
}
```
## Point (Geolocation)
Point fields store geographic coordinates with automatic 2dsphere indexing for geospatial queries.
```ts
import type { PointField } from 'payload'
const locationField: PointField = {
name: 'location',
type: 'point',
label: 'Location',
required: true,
}
// Returns [longitude, latitude]
// Example: [-122.4194, 37.7749] for San Francisco
```
### Geospatial Queries
```ts
// Query by distance (sorted by nearest first)
const nearbyLocations = await payload.find({
collection: 'stores',
where: {
location: {
near: [10, 20], // [longitude, latitude]
maxDistance: 5000, // in meters
minDistance: 1000,
},
},
})
// Query within polygon area
const polygon: Point[] = [
[9.0, 19.0], // bottom-left
[9.0, 21.0], // top-left
[11.0, 21.0], // top-right
[11.0, 19.0], // bottom-right
[9.0, 19.0], // closing point
]
const withinArea = await payload.find({
collection: 'stores',
where: {
location: {
within: {
type: 'Polygon',
coordinates: [polygon],
},
},
},
})
// Query intersecting area
const intersecting = await payload.find({
collection: 'stores',
where: {
location: {
intersects: {
type: 'Polygon',
coordinates: [polygon],
},
},
},
})
```
**Note**: Point fields are not supported in SQLite.
## Join Fields
Join fields create reverse relationships, allowing you to access related documents from the "other side" of a relationship.
```ts
import type { JoinField } from 'payload'
// From Users collection - show user's orders
const ordersJoinField: JoinField = {
name: 'orders',
type: 'join',
collection: 'orders',
on: 'customer', // The field in 'orders' that references this user
admin: {
allowCreate: false,
defaultColumns: ['id', 'createdAt', 'total', 'currency', 'items'],
},
}
// From Users collection - show user's cart
const cartJoinField: JoinField = {
name: 'cart',
type: 'join',
collection: 'carts',
on: 'customer',
admin: {
allowCreate: false,
defaultColumns: ['id', 'createdAt', 'total', 'currency'],
},
}
```
## Virtual Fields
```ts
import type { TextField } from 'payload'
// Computed from siblings
const computedVirtualField: TextField = {
name: 'fullName',
type: 'text',
virtual: true,
hooks: {
afterRead: [({ siblingData }) => `${siblingData.firstName} ${siblingData.lastName}`],
},
}
// From relationship path
const pathVirtualField: TextField = {
name: 'authorName',
type: 'text',
virtual: 'author.name',
}
```
## Conditional Fields
```ts
import type { UploadField, CheckboxField } from 'payload'
// Simple boolean condition
const enableFeatureField: CheckboxField = {
name: 'enableFeature',
type: 'checkbox',
}
const conditionalField: TextField = {
name: 'featureText',
type: 'text',
admin: {
condition: (data) => data.enableFeature === true,
},
}
// Sibling data condition (from hero field pattern)
const typeField: SelectField = {
name: 'type',
type: 'select',
options: ['none', 'highImpact', 'mediumImpact', 'lowImpact'],
defaultValue: 'lowImpact',
}
const mediaField: UploadField = {
name: 'media',
type: 'upload',
relationTo: 'media',
admin: {
condition: (_, { type } = {}) => ['highImpact', 'mediumImpact'].includes(type),
},
required: true,
}
```
## Radio
Radio fields present options as radio buttons for single selection.
```ts
import type { RadioField } from 'payload'
const radioField: RadioField = {
name: 'priority',
type: 'radio',
options: [
{ label: 'Low', value: 'low' },
{ label: 'Medium', value: 'medium' },
{ label: 'High', value: 'high' },
],
defaultValue: 'medium',
admin: {
layout: 'horizontal', // or 'vertical'
},
}
```
## Row (Layout)
Row fields arrange fields horizontally in the admin panel (presentational only).
```ts
import type { RowField } from 'payload'
const rowField: RowField = {
type: 'row',
fields: [
{
name: 'firstName',
type: 'text',
admin: { width: '50%' },
},
{
name: 'lastName',
type: 'text',
admin: { width: '50%' },
},
],
}
```
## Collapsible (Layout)
Collapsible fields group fields in an expandable/collapsible section.
```ts
import type { CollapsibleField } from 'payload'
const collapsibleField: CollapsibleField = {
label: ({ data }) => data?.title || 'Advanced Options',
type: 'collapsible',
admin: {
initCollapsed: true,
},
fields: [
{ name: 'customCSS', type: 'textarea' },
{ name: 'customJS', type: 'code' },
],
}
```
## UI (Custom Components)
UI fields allow fully custom React components in the admin (no data stored).
```ts
import type { UIField } from 'payload'
const uiField: UIField = {
name: 'customMessage',
type: 'ui',
admin: {
components: {
Field: '/path/to/CustomFieldComponent',
Cell: '/path/to/CustomCellComponent', // For list view
},
},
}
```
## Tabs & Groups
```ts
import type { TabsField, GroupField } from 'payload'
// Tabs
const tabsField: TabsField = {
type: 'tabs',
tabs: [
{
label: 'Content',
fields: [
{ name: 'title', type: 'text' },
{ name: 'body', type: 'richText' },
],
},
{
label: 'SEO',
fields: [
{ name: 'metaTitle', type: 'text' },
{ name: 'metaDescription', type: 'textarea' },
],
},
],
}
// Group (named)
const groupField: GroupField = {
name: 'meta',
type: 'group',
fields: [
{ name: 'title', type: 'text' },
{ name: 'description', type: 'textarea' },
],
}
```
## Reusable Field Factories
Create composable field patterns that can be customized with overrides.
```ts
import type { Field, GroupField } from 'payload'
// Utility for deep merging
const deepMerge = <T>(target: T, source: Partial<T>): T => {
// Implementation would deeply merge objects
return { ...target, ...source }
}
// Reusable link field factory
type LinkType = (options?: {
appearances?: ('default' | 'outline')[] | false
disableLabel?: boolean
overrides?: Record<string, unknown>
}) => GroupField
export const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } = {}) => {
const linkField: GroupField = {
name: 'link',
type: 'group',
admin: {
hideGutter: true,
},
fields: [
{
type: 'row',
fields: [
{
name: 'type',
type: 'radio',
options: [
{ label: 'Internal link', value: 'reference' },
{ label: 'Custom URL', value: 'custom' },
],
defaultValue: 'reference',
admin: {
layout: 'horizontal',
width: '50%',
},
},
{
name: 'newTab',
type: 'checkbox',
label: 'Open in new tab',
admin: {
width: '50%',
style: {
alignSelf: 'flex-end',
},
},
},
],
},
{
name: 'reference',
type: 'relationship',
relationTo: ['pages'],
required: true,
maxDepth: 1,
admin: {
condition: (_, siblingData) => siblingData?.type === 'reference',
},
},
{
name: 'url',
type: 'text',
label: 'Custom URL',
required: true,
admin: {
condition: (_, siblingData) => siblingData?.type === 'custom',
},
},
],
}
if (!disableLabel) {
linkField.fields.push({
name: 'label',
type: 'text',
required: true,
})
}
if (appearances !== false) {
linkField.fields.push({
name: 'appearance',
type: 'select',
defaultValue: 'default',
options: [
{ label: 'Default', value: 'default' },
{ label: 'Outline', value: 'outline' },
],
})
}
return deepMerge(linkField, overrides) as GroupField
}
// Usage
const navItem = link({ appearances: false })
const ctaButton = link({
overrides: {
name: 'cta',
admin: {
description: 'Call to action button',
},
},
})
```
## Field Type Guards
Type guards for runtime field type checking and safe type narrowing.
| Type Guard | Checks For | Use When |
| --------------------------- | ----------------------------------------------------------- | ---------------------------------------- |
| `fieldAffectsData` | Field stores data (has name, not UI-only) | Need to access field data or name |
| `fieldHasSubFields` | Field contains nested fields (group/array/row/collapsible) | Need to recursively traverse fields |
| `fieldIsArrayType` | Field is array type | Distinguish arrays from other containers |
| `fieldIsBlockType` | Field is blocks type | Handle blocks-specific logic |
| `fieldIsGroupType` | Field is group type | Handle group-specific logic |
| `fieldSupportsMany` | Field can have multiple values (select/relationship/upload) | Check for `hasMany` support |
| `fieldHasMaxDepth` | Field supports population depth control | Control relationship/upload/join depth |
| `fieldIsPresentationalOnly` | Field is UI-only (no data storage) | Exclude from data operations |
| `fieldIsSidebar` | Field positioned in sidebar | Separate sidebar rendering |
| `fieldIsID` | Field name is 'id' | Special ID field handling |
| `fieldIsHiddenOrDisabled` | Field is hidden or disabled | Filter from UI operations |
| `fieldShouldBeLocalized` | Field needs localization handling | Proper locale table checks |
| `fieldIsVirtual` | Field is virtual (computed/no DB column) | Skip in database transforms |
| `tabHasName` | Tab is named (stores data) | Distinguish named vs unnamed tabs |
| `groupHasName` | Group is named (stores data) | Distinguish named vs unnamed groups |
| `optionIsObject` | Option is `{label, value}` format | Access option properties safely |
| `optionsAreObjects` | All options are objects | Batch option processing |
| `optionIsValue` | Option is string value | Handle string options |
| `valueIsValueWithRelation` | Value is polymorphic relationship | Handle polymorphic relationships |
```ts
import { fieldAffectsData, fieldHasSubFields, fieldIsArrayType } from 'payload'
function processField(field: Field) {
if (fieldAffectsData(field)) {
// Safe to access field.name
console.log(field.name)
}
if (fieldHasSubFields(field)) {
// Safe to access field.fields
field.fields.forEach(processField)
}
}
```
See [FIELD-TYPE-GUARDS.md](FIELD-TYPE-GUARDS.md) for detailed usage patterns.

View File

@@ -0,0 +1,186 @@
# Payload Hooks Reference
Complete reference for collection hooks, field hooks, and hook context patterns.
## Collection Hooks
```ts
export const Posts: CollectionConfig = {
slug: 'posts',
hooks: {
// Before validation
beforeValidate: [
async ({ data, operation }) => {
if (operation === 'create') {
data.slug = slugify(data.title)
}
return data
},
],
// Before save
beforeChange: [
async ({ data, req, operation, originalDoc }) => {
if (operation === 'update' && data.status === 'published') {
data.publishedAt = new Date()
}
return data
},
],
// After save
afterChange: [
async ({ doc, req, operation, previousDoc }) => {
if (operation === 'create') {
await sendNotification(doc)
}
return doc
},
],
// After read
afterRead: [
async ({ doc, req }) => {
doc.viewCount = await getViewCount(doc.id)
return doc
},
],
// Before delete
beforeDelete: [
async ({ req, id }) => {
await cleanupRelatedData(id)
},
],
},
}
```
## Field Hooks
```ts
import type { EmailField, FieldHook } from 'payload'
const beforeValidateHook: FieldHook = ({ value }) => {
return value.trim().toLowerCase()
}
const afterReadHook: FieldHook = ({ value, req }) => {
// Hide email from non-admins
if (!req.user?.roles?.includes('admin')) {
return value.replace(/(.{2})(.*)(@.*)/, '$1***$3')
}
return value
}
const emailField: EmailField = {
name: 'email',
type: 'email',
hooks: {
beforeValidate: [beforeValidateHook],
afterRead: [afterReadHook],
},
}
```
## Hook Context
Share data between hooks or control hook behavior using request context:
```ts
import type { CollectionConfig } from 'payload'
export const Posts: CollectionConfig = {
slug: 'posts',
hooks: {
beforeChange: [
async ({ context }) => {
context.expensiveData = await fetchExpensiveData()
},
],
afterChange: [
async ({ context, doc }) => {
// Reuse from previous hook
await processData(doc, context.expensiveData)
},
],
},
fields: [{ name: 'title', type: 'text' }],
}
```
## Next.js Revalidation with Context Control
```ts
import type { CollectionAfterChangeHook, CollectionAfterDeleteHook } from 'payload'
import { revalidatePath } from 'next/cache'
import type { Page } from '../payload-types'
export const revalidatePage: CollectionAfterChangeHook<Page> = ({
doc,
previousDoc,
req: { payload, context },
}) => {
if (!context.disableRevalidate) {
if (doc._status === 'published') {
const path = doc.slug === 'home' ? '/' : `/${doc.slug}`
payload.logger.info(`Revalidating page at path: ${path}`)
revalidatePath(path)
}
// Revalidate old path if unpublished
if (previousDoc?._status === 'published' && doc._status !== 'published') {
const oldPath = previousDoc.slug === 'home' ? '/' : `/${previousDoc.slug}`
payload.logger.info(`Revalidating old page at path: ${oldPath}`)
revalidatePath(oldPath)
}
}
return doc
}
export const revalidateDelete: CollectionAfterDeleteHook<Page> = ({ doc, req: { context } }) => {
if (!context.disableRevalidate) {
const path = doc?.slug === 'home' ? '/' : `/${doc?.slug}`
revalidatePath(path)
}
return doc
}
```
## Date Field Auto-Set
Automatically set date when document is published:
```ts
import type { DateField } from 'payload'
const publishedOnField: DateField = {
name: 'publishedOn',
type: 'date',
admin: {
date: {
pickerAppearance: 'dayAndTime',
},
position: 'sidebar',
},
hooks: {
beforeChange: [
({ siblingData, value }) => {
if (siblingData._status === 'published' && !value) {
return new Date()
}
return value
},
],
},
}
```
## Hook Patterns Best Practices
- Use `beforeValidate` for data formatting
- Use `beforeChange` for business logic
- Use `afterChange` for side effects
- Use `afterRead` for computed fields
- Store expensive operations in `context`
- Pass `req` to nested operations for transaction safety (see [ADAPTERS.md#threading-req-through-operations](ADAPTERS.md#threading-req-through-operations))

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,274 @@
# Payload Querying Reference
Complete reference for querying data across Local API, REST, and GraphQL.
## Query Operators
```ts
import type { Where } from 'payload'
// Equals
const equalsQuery: Where = { color: { equals: 'blue' } }
// Not equals
const notEqualsQuery: Where = { status: { not_equals: 'draft' } }
// Greater/less than
const greaterThanQuery: Where = { price: { greater_than: 100 } }
const lessThanEqualQuery: Where = { age: { less_than_equal: 65 } }
// Contains (case-insensitive)
const containsQuery: Where = { title: { contains: 'payload' } }
// Like (all words present)
const likeQuery: Where = { description: { like: 'cms headless' } }
// In/not in
const inQuery: Where = { category: { in: ['tech', 'news'] } }
// Exists
const existsQuery: Where = { image: { exists: true } }
// Near (point fields)
const nearQuery: Where = { location: { near: '-122.4194,37.7749,10000' } }
```
## AND/OR Logic
```ts
import type { Where } from 'payload'
const complexQuery: Where = {
or: [
{ color: { equals: 'mint' } },
{
and: [{ color: { equals: 'white' } }, { featured: { equals: false } }],
},
],
}
```
## Nested Properties
```ts
import type { Where } from 'payload'
const nestedQuery: Where = {
'author.role': { equals: 'editor' },
'meta.featured': { exists: true },
}
```
## Local API
```ts
// Find documents
const posts = await payload.find({
collection: 'posts',
where: {
status: { equals: 'published' },
'author.name': { contains: 'john' },
},
depth: 2,
limit: 10,
page: 1,
sort: '-createdAt',
locale: 'en',
select: {
title: true,
author: true,
},
})
// Find by ID
const post = await payload.findByID({
collection: 'posts',
id: '123',
depth: 2,
})
// Create
const post = await payload.create({
collection: 'posts',
data: {
title: 'New Post',
status: 'draft',
},
})
// Update
await payload.update({
collection: 'posts',
id: '123',
data: {
status: 'published',
},
})
// Delete
await payload.delete({
collection: 'posts',
id: '123',
})
// Count
const count = await payload.count({
collection: 'posts',
where: {
status: { equals: 'published' },
},
})
```
### Threading req Parameter
When performing operations in hooks or nested operations, pass the `req` parameter to maintain transaction context:
```ts
// ✅ CORRECT: Pass req for transaction safety
const afterChange: CollectionAfterChangeHook = async ({ doc, req }) => {
await req.payload.create({
collection: 'audit-log',
data: { action: 'created', docId: doc.id },
req, // Maintains transaction atomicity
})
}
// ❌ WRONG: Missing req breaks transaction
const afterChange: CollectionAfterChangeHook = async ({ doc, req }) => {
await req.payload.create({
collection: 'audit-log',
data: { action: 'created', docId: doc.id },
// Missing req - runs in separate transaction
})
}
```
This is critical for MongoDB replica sets and Postgres. See [ADAPTERS.md#threading-req-through-operations](ADAPTERS.md#threading-req-through-operations) for details.
### Access Control in Local API
**Important**: Local API bypasses access control by default (`overrideAccess: true`). When passing a `user` parameter, you must explicitly set `overrideAccess: false` to respect that user's permissions.
```ts
// ❌ WRONG: User is passed but access control is bypassed
const posts = await payload.find({
collection: 'posts',
user: currentUser,
// Missing: overrideAccess: false
// Result: Operation runs with ADMIN privileges, ignoring user's permissions
})
// ✅ CORRECT: Respects user's access control permissions
const posts = await payload.find({
collection: 'posts',
user: currentUser,
overrideAccess: false, // Required to enforce access control
// Result: User only sees posts they have permission to read
})
// Administrative operation (intentionally bypass access control)
const allPosts = await payload.find({
collection: 'posts',
// No user parameter
// overrideAccess defaults to true
// Result: Returns all posts regardless of access control
})
```
**When to use `overrideAccess: false`:**
- Performing operations on behalf of a user
- Testing access control logic
- API routes that should respect user permissions
- Any operation where `user` parameter is provided
**When `overrideAccess: true` is appropriate:**
- Administrative operations (migrations, seeds, cron jobs)
- Internal system operations
- Operations explicitly intended to bypass access control
See [ACCESS-CONTROL.md#important-notes](ACCESS-CONTROL.md#important-notes) for more details.
## REST API
```ts
import { stringify } from 'qs-esm'
const query = {
status: { equals: 'published' },
}
const queryString = stringify(
{
where: query,
depth: 2,
limit: 10,
},
{ addQueryPrefix: true },
)
const response = await fetch(`https://api.example.com/api/posts${queryString}`)
const data = await response.json()
```
### REST Endpoints
```txt
GET /api/{collection} - Find documents
GET /api/{collection}/{id} - Find by ID
POST /api/{collection} - Create
PATCH /api/{collection}/{id} - Update
DELETE /api/{collection}/{id} - Delete
GET /api/{collection}/count - Count documents
GET /api/globals/{slug} - Get global
POST /api/globals/{slug} - Update global
```
## GraphQL
```graphql
query {
Posts(where: { status: { equals: published } }, limit: 10, sort: "-createdAt") {
docs {
id
title
author {
name
}
}
totalDocs
hasNextPage
}
}
mutation {
createPost(data: { title: "New Post", status: draft }) {
id
title
}
}
mutation {
updatePost(id: "123", data: { status: published }) {
id
status
}
}
mutation {
deletePost(id: "123") {
id
}
}
```
## Performance Best Practices
- Set `maxDepth` on relationships to prevent over-fetching
- Use `select` to limit returned fields
- Index frequently queried fields
- Use `virtual` fields for computed data
- Cache expensive operations in hook `context`

3
.pi/trust.json Normal file
View File

@@ -0,0 +1,3 @@
{
"/Users/syntaxbullet": true
}

14
.prettierignore Normal file
View File

@@ -0,0 +1,14 @@
**/payload-types.ts
.tmp
**/.git
**/.hg
**/.pnp.*
**/.svn
**/.yarn/**
**/build
**/dist/**
**/node_modules
**/temp
**/docs/**
tsconfig.json

6
.prettierrc.json Normal file
View File

@@ -0,0 +1,6 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"semi": false
}

3
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
}

24
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,24 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Next.js: debug full stack",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/node_modules/next/dist/bin/next",
"runtimeArgs": ["--inspect"],
"skipFiles": ["<node_internals>/**"],
"serverReadyAction": {
"action": "debugWithChrome",
"killOnServerStop": true,
"pattern": "- Local:.+(https?://.+)",
"uriFormat": "%s",
"webRoot": "${workspaceFolder}"
},
"cwd": "${workspaceFolder}"
}
]
}

41
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,41 @@
{
"npm.packageManager": "pnpm",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"editor.formatOnSaveMode": "file",
"js/ts.tsdk.path": "node_modules/typescript/lib",
"js/ts.tsdk.promptToUseWorkspaceVersion": true,
"[javascript][typescript][typescriptreact]": {
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}
}

71
Dockerfile Normal file
View File

@@ -0,0 +1,71 @@
# To use this Dockerfile, you have to set `output: 'standalone'` in your next.config.js file.
# From https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile
FROM node:22.17.0-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN \
if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Remove this line if you do not have this folder
COPY --from=builder /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
CMD HOSTNAME="0.0.0.0" node server.js

303
README.md Normal file
View File

@@ -0,0 +1,303 @@
# Payload Website Template
This is the official [Payload Website Template](https://github.com/payloadcms/payload/blob/3.x/templates/website). Use it to power websites, blogs, or portfolios from small to enterprise. This repo includes a fully-working backend, enterprise-grade admin panel, and a beautifully designed, production-ready website.
This template is right for you if you are working on:
- A personal or enterprise-grade website, blog, or portfolio
- A content publishing platform with a fully featured publication workflow
- Exploring the capabilities of Payload
Core features:
- [Pre-configured Payload Config](#how-it-works)
- [Authentication](#users-authentication)
- [Access Control](#access-control)
- [Layout Builder](#layout-builder)
- [Draft Preview](#draft-preview)
- [Live Preview](#live-preview)
- [On-demand Revalidation](#on-demand-revalidation)
- [SEO](#seo)
- [Search](#search)
- [Redirects](#redirects)
- [Jobs and Scheduled Publishing](#jobs-and-scheduled-publish)
- [Website](#website)
## Quick Start
To spin up this example locally, follow these steps:
### Clone
If you have not done so already, you need to have standalone copy of this repo on your machine. If you've already cloned this repo, skip to [Development](#development).
Use the `create-payload-app` CLI to clone this template directly to your machine:
```bash
pnpx create-payload-app my-project -t website
```
### Development
1. First [clone the repo](#clone) if you have not done so already
1. `cd my-project && cp .env.example .env` to copy the example environment variables
1. `pnpm install && pnpm dev` to install dependencies and start the dev server
1. open `http://localhost:3000` to open the app in your browser
That's it! Changes made in `./src` will be reflected in your app. Follow the on-screen instructions to login and create your first admin user. Then check out [Production](#production) once you're ready to build and serve your app, and [Deployment](#deployment) when you're ready to go live.
## How it works
The Payload config is tailored specifically to the needs of most websites. It is pre-configured in the following ways:
### Collections
See the [Collections](https://payloadcms.com/docs/configuration/collections) docs for details on how to extend this functionality.
- #### Users (Authentication)
Users are auth-enabled collections that have access to the admin panel and unpublished content. See [Access Control](#access-control) for more details.
For additional help, see the official [Auth Example](https://github.com/payloadcms/payload/tree/3.x/examples/auth) or the [Authentication](https://payloadcms.com/docs/authentication/overview#authentication-overview) docs.
- #### Posts
Posts are used to generate blog posts, news articles, or any other type of content that is published over time. All posts are layout builder enabled so you can generate unique layouts for each post using layout-building blocks, see [Layout Builder](#layout-builder) for more details. Posts are also draft-enabled so you can preview them before publishing them to your website, see [Draft Preview](#draft-preview) for more details.
- #### Pages
All pages are layout builder enabled so you can generate unique layouts for each page using layout-building blocks, see [Layout Builder](#layout-builder) for more details. Pages are also draft-enabled so you can preview them before publishing them to your website, see [Draft Preview](#draft-preview) for more details.
- #### Media
This is the uploads enabled collection used by pages, posts, and projects to contain media like images, videos, downloads, and other assets. It features pre-configured sizes, focal point and manual resizing to help you manage your pictures.
- #### Categories
A taxonomy used to group posts together. Categories can be nested inside of one another, for example "News > Technology". See the official [Payload Nested Docs Plugin](https://payloadcms.com/docs/plugins/nested-docs) for more details.
### Globals
See the [Globals](https://payloadcms.com/docs/configuration/globals) docs for details on how to extend this functionality.
- `Header`
The data required by the header on your front-end like nav links.
- `Footer`
Same as above but for the footer of your site.
## Access control
Basic access control is setup to limit access to various content based based on publishing status.
- `users`: Users can access the admin panel and create or edit content.
- `posts`: Everyone can access published posts, but only users can create, update, or delete them.
- `pages`: Everyone can access published pages, but only users can create, update, or delete them.
For more details on how to extend this functionality, see the [Payload Access Control](https://payloadcms.com/docs/access-control/overview#access-control) docs.
## Layout Builder
Create unique page layouts for any type of content using a powerful layout builder. This template comes pre-configured with the following layout building blocks:
- Hero
- Content
- Media
- Call To Action
- Archive
Each block is fully designed and built into the front-end website that comes with this template. See [Website](#website) for more details.
## Lexical editor
A deep editorial experience that allows complete freedom to focus just on writing content without breaking out of the flow with support for Payload blocks, media, links and other features provided out of the box. See [Lexical](https://payloadcms.com/docs/rich-text/overview) docs.
## Draft Preview
All posts and pages are draft-enabled so you can preview them before publishing them to your website. To do this, these collections use [Versions](https://payloadcms.com/docs/configuration/collections#versions) with `drafts` set to `true`. This means that when you create a new post, project, or page, it will be saved as a draft and will not be visible on your website until you publish it. This also means that you can preview your draft before publishing it to your website. To do this, we automatically format a custom URL which redirects to your front-end to securely fetch the draft version of your content.
Since the front-end of this template is statically generated, this also means that pages, posts, and projects will need to be regenerated as changes are made to published documents. To do this, we use an `afterChange` hook to regenerate the front-end when a document has changed and its `_status` is `published`.
For more details on how to extend this functionality, see the official [Draft Preview Example](https://github.com/payloadcms/payload/tree/3.x/examples/draft-preview).
## Live preview
In addition to draft previews you can also enable live preview to view your end resulting page as you're editing content with full support for SSR rendering. See [Live preview docs](https://payloadcms.com/docs/live-preview/overview) for more details.
## On-demand Revalidation
We've added hooks to collections and globals so that all of your pages, posts, footer, or header changes will automatically be updated in the frontend via on-demand revalidation supported by Nextjs.
> Note: if an image has been changed, for example it's been cropped, you will need to republish the page it's used on in order to be able to revalidate the Nextjs image cache.
## SEO
This template comes pre-configured with the official [Payload SEO Plugin](https://payloadcms.com/docs/plugins/seo) for complete SEO control from the admin panel. All SEO data is fully integrated into the front-end website that comes with this template. See [Website](#website) for more details.
## Search
This template also pre-configured with the official [Payload Search Plugin](https://payloadcms.com/docs/plugins/search) to showcase how SSR search features can easily be implemented into Next.js with Payload. See [Website](#website) for more details.
## Redirects
If you are migrating an existing site or moving content to a new URL, you can use the `redirects` collection to create a proper redirect from old URLs to new ones. This will ensure that proper request status codes are returned to search engines and that your users are not left with a broken link. This template comes pre-configured with the official [Payload Redirects Plugin](https://payloadcms.com/docs/plugins/redirects) for complete redirect control from the admin panel. All redirects are fully integrated into the front-end website that comes with this template. See [Website](#website) for more details.
## Jobs and Scheduled Publish
We have configured [Scheduled Publish](https://payloadcms.com/docs/versions/drafts#scheduled-publish) which uses the [jobs queue](https://payloadcms.com/docs/jobs-queue/jobs) in order to publish or unpublish your content on a scheduled time. The tasks are run on a cron schedule and can also be run as a separate instance if needed.
> Note: When deployed on Vercel, depending on the plan tier, you may be limited to daily cron only.
## Website
This template includes a beautifully designed, production-ready front-end built with the [Next.js App Router](https://nextjs.org), served right alongside your Payload app in a instance. This makes it so that you can deploy both your backend and website where you need it.
Core features:
- [Next.js App Router](https://nextjs.org)
- [TypeScript](https://www.typescriptlang.org)
- [React Hook Form](https://react-hook-form.com)
- [Payload Admin Bar](https://github.com/payloadcms/payload/tree/3.x/packages/admin-bar)
- [TailwindCSS styling](https://tailwindcss.com/)
- [shadcn/ui components](https://ui.shadcn.com/)
- User Accounts and Authentication
- Fully featured blog
- Publication workflow
- Dark mode
- Pre-made layout building blocks
- SEO
- Search
- Redirects
- Live preview
### Cache
Although Next.js includes a robust set of caching strategies out of the box, Payload Cloud proxies and caches all files through Cloudflare using the [Official Cloud Plugin](https://www.npmjs.com/package/@payloadcms/payload-cloud). This means that Next.js caching is not needed and is disabled by default. If you are hosting your app outside of Payload Cloud, you can easily reenable the Next.js caching mechanisms by removing the `no-store` directive from all fetch requests in `./src/app/_api` and then removing all instances of `export const dynamic = 'force-dynamic'` from pages files, such as `./src/app/(pages)/[slug]/page.tsx`. For more details, see the official [Next.js Caching Docs](https://nextjs.org/docs/app/building-your-application/caching).
## Development
To spin up this example locally, follow the [Quick Start](#quick-start). Then [Seed](#seed) the database with a few pages, posts, and projects.
### Working with Postgres
Postgres and other SQL-based databases follow a strict schema for managing your data. In comparison to our MongoDB adapter, this means that there's a few extra steps to working with Postgres.
Note that often times when making big schema changes you can run the risk of losing data if you're not manually migrating it.
#### Local development
Ideally we recommend running a local copy of your database so that schema updates are as fast as possible. By default the Postgres adapter has `push: true` for development environments. This will let you add, modify and remove fields and collections without needing to run any data migrations.
If your database is pointed to production you will want to set `push: false` otherwise you will risk losing data or having your migrations out of sync.
#### Migrations
[Migrations](https://payloadcms.com/docs/database/migrations) are essentially SQL code versions that keeps track of your schema. When deploy with Postgres you will need to make sure you create and then run your migrations.
Locally create a migration
```bash
pnpm payload migrate:create
```
This creates the migration files you will need to push alongside with your new configuration.
On the server after building and before running `pnpm start` you will want to run your migrations
```bash
pnpm payload migrate
```
This command will check for any migrations that have not yet been run and try to run them and it will keep a record of migrations that have been run in the database.
### Docker
Alternatively, you can use [Docker](https://www.docker.com) to spin up this template locally. To do so, follow these steps:
1. Follow [steps 1 and 2 from above](#development), the docker-compose file will automatically use the `.env` file in your project root
1. Next run `docker-compose up`
1. Follow [steps 4 and 5 from above](#development) to login and create your first admin user
That's it! The Docker instance will help you get up and running quickly while also standardizing the development environment across your teams.
### Seed
To seed the database with a few pages, posts, and projects you can click the 'seed database' link from the admin panel.
The seed script will also create a demo user for demonstration purposes only:
- Demo Author
- Email: `demo-author@payloadcms.com`
- Password: `password`
> NOTICE: seeding the database is destructive because it drops your current database to populate a fresh one from the seed template. Only run this command if you are starting a new project or can afford to lose your current data.
## Production
To run Payload in production, you need to build and start the Admin panel. To do so, follow these steps:
1. Invoke the `next build` script by running `pnpm build` or `npm run build` in your project root. This creates a `.next` directory with a production-ready admin bundle.
1. Finally run `pnpm start` or `npm run start` to run Node in production and serve Payload from the `.build` directory.
1. When you're ready to go live, see Deployment below for more details.
### Deploying to Vercel
This template can also be deployed to Vercel for free. You can get started by choosing the Vercel DB adapter during the setup of the template or by manually installing and configuring it:
```bash
pnpm add @payloadcms/db-vercel-postgres
```
```ts
// payload.config.ts
import { vercelPostgresAdapter } from '@payloadcms/db-vercel-postgres'
export default buildConfig({
// ...
db: vercelPostgresAdapter({
pool: {
connectionString: process.env.POSTGRES_URL || '',
},
}),
// ...
```
We also support Vercel's blob storage:
```bash
pnpm add @payloadcms/storage-vercel-blob
```
```ts
// payload.config.ts
import { vercelBlobStorage } from '@payloadcms/storage-vercel-blob'
export default buildConfig({
// ...
plugins: [
vercelBlobStorage({
collections: {
[Media.slug]: true,
},
token: process.env.BLOB_READ_WRITE_TOKEN || '',
}),
],
// ...
```
There is also a simplified [one click deploy](https://github.com/payloadcms/payload/tree/3.x/templates/with-vercel-postgres) to Vercel should you need it.
### Self-hosting
Before deploying your app, you need to:
1. Ensure your app builds and serves in production. See [Production](#production) for more details.
2. You can then deploy Payload as you would any other Node.js or Next.js application either directly on a VPS, DigitalOcean's Apps Platform, via Coolify or more. More guides coming soon.
You can also deploy your app manually, check out the [deployment documentation](https://payloadcms.com/docs/production/deployment) for full details.
## Questions
If you have any issues or questions, reach out to us on [Discord](https://discord.com/invite/payload) or start a [GitHub discussion](https://github.com/payloadcms/payload/discussions).

457
bun.lock
View File

@@ -1,457 +0,0 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "bmp-website",
"dependencies": {
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.4",
"@uidotdev/usehooks": "^2.4.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"embla-carousel-auto-scroll": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"framer-motion": "^12.36.0",
"lucide-react": "^0.577.0",
"motion": "^12.36.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router-dom": "^7.13.1",
"tailwind-merge": "^3.5.0",
},
"devDependencies": {
"@tailwindcss/vite": "^4.2.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.4",
"tailwindcss": "^4.2.1",
"vite": "^5.4.0",
},
},
},
"packages": {
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
"@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
"@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="],
"@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
"@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="],
"@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="],
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="],
"@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "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" } }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
"@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "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" } }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="],
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "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" } }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "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" } }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "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" } }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "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" } }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="],
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "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" } }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "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" } }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "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" } }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "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" } }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.3.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "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" } }, "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ=="],
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "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" } }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "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" } }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="],
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "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" } }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
"@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
"@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="],
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
"@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="],
"@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="],
"@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="],
"@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "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" } }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="],
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
"@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="],
"@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@uidotdev/usehooks": ["@uidotdev/usehooks@2.4.1", "", { "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="],
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": "dist/cli.cjs" }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="],
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": "cli.js" }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
"caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="],
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
"electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="],
"embla-carousel": ["embla-carousel@8.6.0", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="],
"embla-carousel-auto-scroll": ["embla-carousel-auto-scroll@8.6.0", "", { "peerDependencies": { "embla-carousel": "8.6.0" } }, "sha512-WT9fWhNXFpbQ6kP+aS07oF5IHYLZ1Dx4DkwgCY8Hv2ZyYd2KMCPfMV1q/cA3wFGuLO7GMgKiySLX90/pQkcOdQ=="],
"embla-carousel-react": ["embla-carousel-react@8.6.0", "", { "dependencies": { "embla-carousel": "8.6.0", "embla-carousel-reactive-utils": "8.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA=="],
"embla-carousel-reactive-utils": ["embla-carousel-reactive-utils@8.6.0", "", { "peerDependencies": { "embla-carousel": "8.6.0" } }, "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A=="],
"enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="],
"esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": "bin/esbuild" }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"framer-motion": ["framer-motion@12.36.0", "", { "dependencies": { "motion-dom": "^12.36.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid"] }, "sha512-4PqYHAT7gev0ke0wos+PyrcFxI0HScjm3asgU8nSYa8YzJFuwgIvdj3/s3ZaxLq0bUSboIn19A2WS/MHwLCvfw=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"jiti": ["jiti@2.6.1", "", { "bin": "lib/jiti-cli.mjs" }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": "bin/jsesc" }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json5": ["json5@2.2.3", "", { "bin": "lib/cli.js" }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"lucide-react": ["lucide-react@0.577.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"motion": ["motion@12.36.0", "", { "dependencies": { "framer-motion": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid"] }, "sha512-5BMQuktYUX8aEByKWYx5tR4X3G08H2OMgp46wTxZ4o7CDDstyy4A0fe9RLNMjZiwvntCWGDvs16sC87/emz4Yw=="],
"motion-dom": ["motion-dom@12.36.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-Ep1pq8P88rGJ75om8lTCA13zqd7ywPGwCqwuWwin6BKc0hMLkVfcS6qKlRqEo2+t0DwoUcgGJfXwaiFn4AOcQA=="],
"motion-utils": ["motion-utils@12.36.0", "", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
"react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
"react-router": ["react-router@7.13.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA=="],
"react-router-dom": ["react-router-dom@7.13.1", "", { "dependencies": { "react-router": "7.13.1" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw=="],
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
"rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
"tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
"vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": "bin/vite.js" }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "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" } }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "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" } }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
"@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "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" } }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "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" } }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
}
}

17
components.json Normal file
View File

@@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.mjs",
"css": "src/app/(frontend)/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/utilities/ui"
}
}

31
docker-compose.yml Normal file
View File

@@ -0,0 +1,31 @@
version: '3'
services:
payload:
image: node:18-alpine
ports:
- '3000:3000'
volumes:
- .:/home/node/app
- node_modules:/home/node/app/node_modules
working_dir: /home/node/app/
command: sh -c "yarn install && yarn dev"
depends_on:
- mongo
env_file:
- .env
mongo:
image: mongo:latest
ports:
- '27017:27017'
command:
- --storageEngine=wiredTiger
volumes:
- data:/data/db
logging:
driver: none
volumes:
data:
node_modules:

38
eslint.config.mjs Normal file
View File

@@ -0,0 +1,38 @@
import { dirname } from 'path'
import { fileURLToPath } from 'url'
import { FlatCompat } from '@eslint/eslintrc'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const compat = new FlatCompat({
baseDirectory: __dirname,
})
const eslintConfig = [
...compat.extends('next/core-web-vitals', 'next/typescript'),
{
rules: {
'@typescript-eslint/ban-ts-comment': 'warn',
'@typescript-eslint/no-empty-object-type': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': [
'warn',
{
vars: 'all',
args: 'after-used',
ignoreRestSiblings: false,
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^(_|ignore)',
},
],
},
},
{
ignores: ['.next/', 'src/payload-types.ts', 'src/payload-generated-schema.ts'],
},
]
export default eslintConfig

View File

@@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bayerischer Mittelstandspreis – Exzellenz im Mittelstand</title>
<meta name="description" content="Der Bayerische Mittelstandspreis (BMP) zeichnet herausragende mittelständische Unternehmen in Bayern aus. Jetzt bewerben und Ihre Erfolgsgeschichte teilen.">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

20
next-sitemap.config.cjs Normal file
View File

@@ -0,0 +1,20 @@
const SITE_URL =
process.env.NEXT_PUBLIC_SERVER_URL ||
process.env.VERCEL_PROJECT_PRODUCTION_URL ||
'https://example.com'
/** @type {import('next-sitemap').IConfig} */
module.exports = {
siteUrl: SITE_URL,
generateRobotsTxt: true,
exclude: ['/posts-sitemap.xml', '/pages-sitemap.xml', '/*', '/posts/*'],
robotsTxtOptions: {
policies: [
{
userAgent: '*',
disallow: '/admin/*',
},
],
additionalSitemaps: [`${SITE_URL}/pages-sitemap.xml`, `${SITE_URL}/posts-sitemap.xml`],
},
}

54
next.config.ts Normal file
View File

@@ -0,0 +1,54 @@
import { withPayload } from '@payloadcms/next/withPayload'
import type { NextConfig } from 'next'
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(__filename)
import { redirects } from './redirects'
const NEXT_PUBLIC_SERVER_URL = process.env.VERCEL_PROJECT_PRODUCTION_URL
? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
: process.env.__NEXT_PRIVATE_ORIGIN || 'http://localhost:3000'
const nextConfig: NextConfig = {
// Temporarily required on Windows until Next.js fixes Turbopack Sass resolution.
// See: https://github.com/vercel/next.js/issues/86431
sassOptions: {
loadPaths: ['./node_modules/@payloadcms/ui/dist/scss/'],
},
images: {
localPatterns: [
{
pathname: '/api/media/file/**',
},
],
qualities: [100],
remotePatterns: [
...[NEXT_PUBLIC_SERVER_URL /* 'https://example.com' */].map((item) => {
const url = new URL(item)
return {
hostname: url.hostname,
protocol: url.protocol.replace(':', '') as 'http' | 'https',
}
}),
],
},
webpack: (webpackConfig) => {
webpackConfig.resolve.extensionAlias = {
'.cjs': ['.cts', '.cjs'],
'.js': ['.ts', '.tsx', '.js', '.jsx'],
'.mjs': ['.mts', '.mjs'],
}
return webpackConfig
},
reactStrictMode: true,
redirects,
turbopack: {
root: path.resolve(dirname),
},
}
export default withPayload(nextConfig, { devBundleServerPackages: false })

13690
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +1,98 @@
{
"name": "bmp-website",
"private": true,
"name": "bmp-website-payloadcms",
"version": "1.0.0",
"description": "Website template for Payload",
"license": "MIT",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"@tailwindcss/vite": "^4.2.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.4",
"tailwindcss": "^4.2.1",
"vite": "^5.4.0"
"build": "cross-env NODE_OPTIONS=--no-deprecation next build",
"postbuild": "next-sitemap --config next-sitemap.config.cjs",
"dev": "cross-env NODE_OPTIONS=--no-deprecation next dev",
"dev:prod": "cross-env NODE_OPTIONS=--no-deprecation rm -rf .next && pnpm build && pnpm start",
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
"ii": "cross-env NODE_OPTIONS=--no-deprecation pnpm --ignore-workspace install",
"lint": "cross-env NODE_OPTIONS=--no-deprecation eslint .",
"lint:fix": "cross-env NODE_OPTIONS=--no-deprecation eslint . --fix",
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
"reinstall": "cross-env NODE_OPTIONS=--no-deprecation rm -rf node_modules && rm pnpm-lock.yaml && pnpm --ignore-workspace install",
"start": "cross-env NODE_OPTIONS=--no-deprecation next start",
"test": "pnpm run test:int && pnpm run test:e2e",
"test:e2e": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" playwright test --config=playwright.config.ts",
"test:int": "cross-env NODE_OPTIONS=--no-deprecation vitest run --config ./vitest.config.mts"
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.4",
"@uidotdev/usehooks": "^2.4.1",
"class-variance-authority": "^0.7.1",
"@payloadcms/admin-bar": "3.85.1",
"@payloadcms/live-preview-react": "3.85.1",
"@payloadcms/next": "3.85.1",
"@payloadcms/plugin-form-builder": "3.85.1",
"@payloadcms/plugin-nested-docs": "3.85.1",
"@payloadcms/plugin-redirects": "3.85.1",
"@payloadcms/plugin-search": "3.85.1",
"@payloadcms/plugin-seo": "3.85.1",
"@payloadcms/richtext-lexical": "3.85.1",
"@payloadcms/ui": "3.85.1",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cross-env": "^7.0.3",
"dotenv": "16.4.7",
"geist": "^1.3.0",
"graphql": "^16.8.2",
"lucide-react": "0.563.0",
"next": "16.2.6",
"next-sitemap": "^4.2.3",
"payload": "3.85.1",
"prism-react-renderer": "^2.3.1",
"react": "19.2.6",
"react-dom": "19.2.6",
"react-hook-form": "7.71.1",
"sharp": "0.34.2",
"tailwind-merge": "^3.4.0",
"@payloadcms/db-sqlite": "3.85.1",
"@uidotdev/usehooks": "^2.4.1",
"embla-carousel-auto-scroll": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"framer-motion": "^12.36.0",
"lucide-react": "^0.577.0",
"motion": "^12.36.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router-dom": "^7.13.1",
"tailwind-merge": "^3.5.0"
"@radix-ui/react-radio-group": "^1.3.8"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@playwright/test": "1.58.2",
"@tailwindcss/postcss": "^4.1.18",
"@tailwindcss/typography": "^0.5.19",
"@testing-library/react": "16.3.0",
"@types/escape-html": "^1.0.2",
"@types/node": "22.19.9",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "4.5.2",
"autoprefixer": "^10.4.19",
"eslint": "^9.16.0",
"eslint-config-next": "16.2.6",
"jsdom": "28.0.0",
"postcss": "^8.4.38",
"prettier": "^3.4.2",
"tailwindcss": "^4.1.18",
"tsx": "4.22.4",
"tw-animate-css": "^1.4.0",
"typescript": "5.7.3",
"vite-tsconfig-paths": "6.0.5",
"vitest": "4.0.18"
},
"engines": {
"node": "^18.20.2 || >=20.9.0",
"pnpm": "^9 || ^10"
},
"pnpm": {
"onlyBuiltDependencies": [
"sharp",
"esbuild",
"unrs-resolver"
]
}
}

41
playwright.config.ts Normal file
View File

@@ -0,0 +1,41 @@
import { defineConfig, devices } from '@playwright/test'
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
import 'dotenv/config'
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests/e2e',
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'], channel: 'chromium' },
},
],
webServer: {
command: 'pnpm dev',
reuseExistingServer: true,
url: 'http://localhost:3000',
},
})

7
postcss.config.js Normal file
View File

@@ -0,0 +1,7 @@
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
}
export default config

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

23
public/favicon.svg Normal file
View File

@@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="1000" height="1000"><style>
#light-icon {
display: inline;
}
#dark-icon {
display: none;
}
@media (prefers-color-scheme: dark) {
#light-icon {
display: none;
}
#dark-icon {
display: inline;
}
}
</style><g id="light-icon"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="1000" height="1000"><g clip-path="url(#SvgjsClipPath1059)"><rect width="1000" height="1000" fill="#000000"></rect><g transform="matrix(5,0,0,5,192.5,150)"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="123" height="140"><svg width="123" height="140" viewBox="0 0 123 140" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M60.2569 118.758L18.9035 94.9917C18.4016 94.6917 18.067 94.1583 18.067 93.5583V56.825C18.067 56.1917 18.7696 55.7917 19.3049 56.0917L67.3164 83.6917C67.9855 84.0917 68.822 83.5917 68.822 82.825V64.925C68.822 64.225 68.4539 63.5583 67.8182 63.1917L10.0707 29.9917C9.56883 29.6917 8.89968 29.6917 8.39782 29.9917L0.836436 34.3583C0.334574 34.6583 0 35.1917 0 35.7917V104.025C0 104.625 0.334574 105.158 0.836436 105.458L60.1565 139.592C60.6583 139.892 61.3275 139.892 61.8293 139.592L111.647 110.925C112.317 110.525 112.317 109.592 111.647 109.192L96.1232 100.258C95.4875 99.8917 94.7515 99.8917 94.1158 100.258L61.9632 118.758C61.4613 119.058 60.7922 119.058 60.2903 118.758H60.2569Z" fill="white"></path>
<path d="M121.149 34.325L61.8294 0.225C61.3275 -0.075 60.6584 -0.075 60.1565 0.225L28.8069 18.2583C28.1378 18.6583 28.1378 19.5917 28.8069 19.9917L44.1973 28.8583C44.833 29.225 45.5691 29.225 46.2048 28.8583L60.2569 20.7917C60.7588 20.4917 61.4279 20.4917 61.9298 20.7917L103.283 44.5583C103.785 44.8583 104.12 45.3917 104.12 45.9917V82.8917C104.12 83.5917 104.488 84.2583 105.123 84.625L120.514 93.4583C121.183 93.8583 122.019 93.3583 122.019 92.5917V35.7917C122.019 35.1917 121.685 34.6583 121.183 34.3583L121.149 34.325Z" fill="white"></path>
</svg></svg></g></g><defs><clipPath id="SvgjsClipPath1059"><rect width="1000" height="1000" x="0" y="0" rx="350" ry="350"></rect></clipPath></defs></svg></g><g id="dark-icon"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="1000" height="1000"><g clip-path="url(#SvgjsClipPath1060)"><rect width="1000" height="1000" fill="#000000"></rect><g transform="matrix(5,0,0,5,192.5,150)"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="123" height="140"><svg width="123" height="140" viewBox="0 0 123 140" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M60.2569 118.758L18.9035 94.9917C18.4016 94.6917 18.067 94.1583 18.067 93.5583V56.825C18.067 56.1917 18.7696 55.7917 19.3049 56.0917L67.3164 83.6917C67.9855 84.0917 68.822 83.5917 68.822 82.825V64.925C68.822 64.225 68.4539 63.5583 67.8182 63.1917L10.0707 29.9917C9.56883 29.6917 8.89968 29.6917 8.39782 29.9917L0.836436 34.3583C0.334574 34.6583 0 35.1917 0 35.7917V104.025C0 104.625 0.334574 105.158 0.836436 105.458L60.1565 139.592C60.6583 139.892 61.3275 139.892 61.8293 139.592L111.647 110.925C112.317 110.525 112.317 109.592 111.647 109.192L96.1232 100.258C95.4875 99.8917 94.7515 99.8917 94.1158 100.258L61.9632 118.758C61.4613 119.058 60.7922 119.058 60.2903 118.758H60.2569Z" fill="white"></path>
<path d="M121.149 34.325L61.8294 0.225C61.3275 -0.075 60.6584 -0.075 60.1565 0.225L28.8069 18.2583C28.1378 18.6583 28.1378 19.5917 28.8069 19.9917L44.1973 28.8583C44.833 29.225 45.5691 29.225 46.2048 28.8583L60.2569 20.7917C60.7588 20.4917 61.4279 20.4917 61.9298 20.7917L103.283 44.5583C103.785 44.8583 104.12 45.3917 104.12 45.9917V82.8917C104.12 83.5917 104.488 84.2583 105.123 84.625L120.514 93.4583C121.183 93.8583 122.019 93.3583 122.019 92.5917V35.7917C122.019 35.1917 121.685 34.6583 121.183 34.3583L121.149 34.325Z" fill="white"></path>
</svg></svg></g></g><defs><clipPath id="SvgjsClipPath1060"><rect width="1000" height="1000" x="0" y="0" rx="350" ry="350"></rect></clipPath></defs></svg></g></svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

18
redirects.ts Normal file
View File

@@ -0,0 +1,18 @@
import type { NextConfig } from 'next'
export const redirects: NextConfig['redirects'] = async () => {
const internetExplorerRedirect = {
destination: '/ie-incompatible.html',
has: [
{
type: 'header' as const,
key: 'user-agent',
value: '(.*Trident.*)', // all ie browsers
},
],
permanent: false,
source: '/:path((?!ie-incompatible.html$).*)', // all pages except the incompatibility page
}
return [internetExplorerRedirect]
}

View File

@@ -1,75 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Routes, Route, useLocation } from 'react-router-dom';
import Layout from '@/components/layout/Layout';
import ScrollToTop from '@/components/layout/ScrollToTop';
import Home from '@/pages/Home';
import Participation from '@/pages/Participation';
import About from '@/pages/About';
import Preistraeger from '@/pages/Preistraeger';
import PreistraegerDetail from '@/pages/PreistraegerDetail';
import Press from '@/pages/Press';
import EventDetail from './pages/EventDetail';
import BlogDetail from './pages/BlogDetail';
import Contact from '@/pages/Contact';
import FormularUpload from '@/pages/FormularUpload';
import Netzwerk from '@/pages/Netzwerk';
import MitgliedWerden from '@/pages/MitgliedWerden';
import Datenschutz from '@/pages/Datenschutz';
import Impressum from '@/pages/Impressum';
const App: React.FC = () => {
const [loading, setLoading] = useState(true);
const location = useLocation();
useEffect(() => {
// Basic loading simulation for splash
const timer = setTimeout(() => {
setLoading(false);
}, 1500);
return () => clearTimeout(timer);
}, []);
if (loading) {
return (
<div className="fixed inset-0 z-[1000] bg-primary flex flex-col items-center justify-center">
<div className="relative">
<div className="w-24 h-24 border-2 border-accent/20 border-t-accent rounded-full animate-spin"></div>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-accent font-display font-bold text-2xl">BMP</span>
</div>
</div>
<div className="mt-8 text-white/40 uppercase tracking-[0.4em] text-[10px] font-bold animate-pulse">
Exzellenz wird geladen...
</div>
</div>
);
}
return (
<>
<ScrollToTop />
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/teilnahme" element={<Participation />} />
<Route path="/preistraeger" element={<Preistraeger />} />
<Route path="/preistraeger/:slug" element={<PreistraegerDetail />} />
<Route path="/der-bmp" element={<About />} />
<Route path="/netzwerk" element={<Netzwerk />} />
<Route path="/presse" element={<Press />} />
<Route path="/presse/events/:slug" element={<EventDetail />} />
<Route path="/presse/blog/:slug" element={<BlogDetail />} />
<Route path="/kontakt" element={<Contact />} />
<Route path="/formular-hochladen" element={<FormularUpload />} />
<Route path="/mitglied-werden" element={<MitgliedWerden />} />
<Route path="/datenschutz" element={<Datenschutz />} />
<Route path="/impressum" element={<Impressum />} />
{/* Fallback to home */}
<Route path="*" element={<Home />} />
</Routes>
</Layout>
</>
);
};
export default App;

32
src/Footer/Component.tsx Normal file
View File

@@ -0,0 +1,32 @@
import { getCachedGlobal } from '@/utilities/getGlobals'
import Link from 'next/link'
import React from 'react'
import { ThemeSelector } from '@/providers/Theme/ThemeSelector'
import { CMSLink } from '@/components/Link'
import { Logo } from '@/components/Logo/Logo'
export async function Footer() {
const footerData = await getCachedGlobal('footer', 1)()
const navItems = footerData?.navItems || []
return (
<footer className="mt-auto border-t border-border bg-black dark:bg-card text-white">
<div className="container py-8 gap-8 flex flex-col md:flex-row md:justify-between">
<Link className="flex items-center" href="/">
<Logo />
</Link>
<div className="flex flex-col-reverse items-start md:flex-row gap-4 md:items-center">
<ThemeSelector />
<nav className="flex flex-col md:flex-row gap-4">
{navItems.map(({ link }, i) => {
return <CMSLink className="text-white" key={i} {...link} />
})}
</nav>
</div>
</div>
</footer>
)
}

13
src/Footer/RowLabel.tsx Normal file
View File

@@ -0,0 +1,13 @@
'use client'
import { Header } from '@/payload-types'
import { RowLabelProps, useRowLabel } from '@payloadcms/ui'
export const RowLabel: React.FC<RowLabelProps> = () => {
const data = useRowLabel<NonNullable<Header['navItems']>[number]>()
const label = data?.data?.link?.label
? `Nav item ${data.rowNumber !== undefined ? data.rowNumber + 1 : ''}: ${data?.data?.link?.label}`
: 'Row'
return <div>{label}</div>
}

32
src/Footer/config.ts Normal file
View File

@@ -0,0 +1,32 @@
import type { GlobalConfig } from 'payload'
import { link } from '@/fields/link'
import { revalidateFooter } from './hooks/revalidateFooter'
export const Footer: GlobalConfig = {
slug: 'footer',
access: {
read: () => true,
},
fields: [
{
name: 'navItems',
type: 'array',
fields: [
link({
appearances: false,
}),
],
maxRows: 6,
admin: {
initCollapsed: true,
components: {
RowLabel: '@/Footer/RowLabel#RowLabel',
},
},
},
],
hooks: {
afterChange: [revalidateFooter],
},
}

View File

@@ -0,0 +1,13 @@
import type { GlobalAfterChangeHook } from 'payload'
import { revalidateTag } from 'next/cache'
export const revalidateFooter: GlobalAfterChangeHook = ({ doc, req: { payload, context } }) => {
if (!context.disableRevalidate) {
payload.logger.info(`Revalidating footer`)
revalidateTag('global_footer', 'max')
}
return doc
}

View File

@@ -0,0 +1,42 @@
'use client'
import { useHeaderTheme } from '@/providers/HeaderTheme'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import React, { useEffect, useState } from 'react'
import type { Header } from '@/payload-types'
import { Logo } from '@/components/Logo/Logo'
import { HeaderNav } from './Nav'
interface HeaderClientProps {
data: Header
}
export const HeaderClient: React.FC<HeaderClientProps> = ({ data }) => {
/* Storing the value in a useState to avoid hydration errors */
const [theme, setTheme] = useState<string | null>(null)
const { headerTheme, setHeaderTheme } = useHeaderTheme()
const pathname = usePathname()
useEffect(() => {
setHeaderTheme(null)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname])
useEffect(() => {
if (headerTheme && headerTheme !== theme) setTheme(headerTheme)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [headerTheme])
return (
<header className="container relative z-20 " {...(theme ? { 'data-theme': theme } : {})}>
<div className="py-8 flex justify-between">
<Link href="/">
<Logo loading="eager" priority="high" className="invert dark:invert-0" />
</Link>
<HeaderNav data={data} />
</div>
</header>
)
}

9
src/Header/Component.tsx Normal file
View File

@@ -0,0 +1,9 @@
import { HeaderClient } from './Component.client'
import { getCachedGlobal } from '@/utilities/getGlobals'
import React from 'react'
export async function Header() {
const headerData = await getCachedGlobal('header', 1)()
return <HeaderClient data={headerData} />
}

25
src/Header/Nav/index.tsx Normal file
View File

@@ -0,0 +1,25 @@
'use client'
import React from 'react'
import type { Header as HeaderType } from '@/payload-types'
import { CMSLink } from '@/components/Link'
import Link from 'next/link'
import { SearchIcon } from 'lucide-react'
export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
const navItems = data?.navItems || []
return (
<nav className="flex gap-3 items-center">
{navItems.map(({ link }, i) => {
return <CMSLink key={i} {...link} appearance="link" />
})}
<Link href="/search">
<span className="sr-only">Search</span>
<SearchIcon className="w-5 text-primary" />
</Link>
</nav>
)
}

13
src/Header/RowLabel.tsx Normal file
View File

@@ -0,0 +1,13 @@
'use client'
import { Header } from '@/payload-types'
import { RowLabelProps, useRowLabel } from '@payloadcms/ui'
export const RowLabel: React.FC<RowLabelProps> = () => {
const data = useRowLabel<NonNullable<Header['navItems']>[number]>()
const label = data?.data?.link?.label
? `Nav item ${data.rowNumber !== undefined ? data.rowNumber + 1 : ''}: ${data?.data?.link?.label}`
: 'Row'
return <div>{label}</div>
}

32
src/Header/config.ts Normal file
View File

@@ -0,0 +1,32 @@
import type { GlobalConfig } from 'payload'
import { link } from '@/fields/link'
import { revalidateHeader } from './hooks/revalidateHeader'
export const Header: GlobalConfig = {
slug: 'header',
access: {
read: () => true,
},
fields: [
{
name: 'navItems',
type: 'array',
fields: [
link({
appearances: false,
}),
],
maxRows: 6,
admin: {
initCollapsed: true,
components: {
RowLabel: '@/Header/RowLabel#RowLabel',
},
},
},
],
hooks: {
afterChange: [revalidateHeader],
},
}

View File

@@ -0,0 +1,13 @@
import type { GlobalAfterChangeHook } from 'payload'
import { revalidateTag } from 'next/cache'
export const revalidateHeader: GlobalAfterChangeHook = ({ doc, req: { payload, context } }) => {
if (!context.disableRevalidate) {
payload.logger.info(`Revalidating header`)
revalidateTag('global_header', 'max')
}
return doc
}

3
src/access/anyone.ts Normal file
View File

@@ -0,0 +1,3 @@
import type { Access } from 'payload'
export const anyone: Access = () => true

View File

@@ -0,0 +1,9 @@
import type { AccessArgs } from 'payload'
import type { User } from '@/payload-types'
type isAuthenticated = (args: AccessArgs<User>) => boolean
export const authenticated: isAuthenticated = ({ req: { user } }) => {
return Boolean(user)
}

View File

@@ -0,0 +1,13 @@
import type { Access } from 'payload'
export const authenticatedOrPublished: Access = ({ req: { user } }) => {
if (user) {
return true
}
return {
_status: {
equals: 'published',
},
}
}

View File

@@ -0,0 +1,68 @@
import { getServerSideSitemap } from 'next-sitemap'
import { getPayload } from 'payload'
import config from '@payload-config'
import { unstable_cache } from 'next/cache'
const getPagesSitemap = unstable_cache(
async () => {
const payload = await getPayload({ config })
const SITE_URL =
process.env.NEXT_PUBLIC_SERVER_URL ||
process.env.VERCEL_PROJECT_PRODUCTION_URL ||
'https://example.com'
const results = await payload.find({
collection: 'pages',
overrideAccess: false,
draft: false,
depth: 0,
limit: 1000,
pagination: false,
where: {
_status: {
equals: 'published',
},
},
select: {
slug: true,
updatedAt: true,
},
})
const dateFallback = new Date().toISOString()
const defaultSitemap = [
{
loc: `${SITE_URL}/search`,
lastmod: dateFallback,
},
{
loc: `${SITE_URL}/posts`,
lastmod: dateFallback,
},
]
const sitemap = results.docs
? results.docs
.filter((page) => Boolean(page?.slug))
.map((page) => {
return {
loc: page?.slug === 'home' ? `${SITE_URL}/` : `${SITE_URL}/${page?.slug}`,
lastmod: page.updatedAt || dateFallback,
}
})
: []
return [...defaultSitemap, ...sitemap]
},
['pages-sitemap'],
{
tags: ['pages-sitemap'],
},
)
export async function GET() {
const sitemap = await getPagesSitemap()
return getServerSideSitemap(sitemap)
}

View File

@@ -0,0 +1,55 @@
import { getServerSideSitemap } from 'next-sitemap'
import { getPayload } from 'payload'
import config from '@payload-config'
import { unstable_cache } from 'next/cache'
const getPostsSitemap = unstable_cache(
async () => {
const payload = await getPayload({ config })
const SITE_URL =
process.env.NEXT_PUBLIC_SERVER_URL ||
process.env.VERCEL_PROJECT_PRODUCTION_URL ||
'https://example.com'
const results = await payload.find({
collection: 'posts',
overrideAccess: false,
draft: false,
depth: 0,
limit: 1000,
pagination: false,
where: {
_status: {
equals: 'published',
},
},
select: {
slug: true,
updatedAt: true,
},
})
const dateFallback = new Date().toISOString()
const sitemap = results.docs
? results.docs
.filter((post) => Boolean(post?.slug))
.map((post) => ({
loc: `${SITE_URL}/posts/${post?.slug}`,
lastmod: post.updatedAt || dateFallback,
}))
: []
return sitemap
},
['posts-sitemap'],
{
tags: ['posts-sitemap'],
},
)
export async function GET() {
const sitemap = await getPostsSitemap()
return getServerSideSitemap(sitemap)
}

View File

@@ -0,0 +1,12 @@
import NextApp from '@/spa/NextApp'
type Args = {
params: Promise<{ slug?: string[] }>
}
export default async function Page({ params }: Args) {
const { slug = [] } = await params
const pathname = `/${slug.join('/')}`.replace(/\/$/, '') || '/'
return <NextApp pathname={pathname} />
}

View File

@@ -0,0 +1,124 @@
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');
@import 'tailwindcss';
@theme {
--font-display: "IBM Plex Sans", sans-serif;
--font-body: "Inter", sans-serif;
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--radius-lg: 0.5rem;
--radius-md: calc(0.5rem - 2px);
--radius-sm: calc(0.5rem - 4px);
}
:root {
--background: #E4E2E3;
--foreground: #020617;
--card: #ffffff;
--card-foreground: #020617;
--popover: #ffffff;
--popover-foreground: #020617;
--primary: #03093A;
--primary-foreground: #ffffff;
--secondary: #f8fafc;
--secondary-foreground: #03093A;
--muted: #f1f5f9;
--muted-foreground: #64748b;
--accent: #EFBF04;
--accent-foreground: #ffffff;
--destructive: #ef4444;
--border: #e2e8f0;
--input: #e2e8f0;
--ring: #03093A;
}
.dark {
--background: #020617;
--foreground: #f8fafc;
--card: #1e293b;
--card-foreground: #f8fafc;
--popover: #020617;
--popover-foreground: #f8fafc;
--primary: #03093A;
--primary-foreground: #ffffff;
--secondary: #1e293b;
--secondary-foreground: #f8fafc;
--muted: #1e293b;
--muted-foreground: #94a3b8;
--accent: #EFBF04;
--accent-foreground: #ffffff;
--destructive: #7f1d1d;
--border: rgba(255, 255, 255, 0.1);
--input: rgba(255, 255, 255, 0.15);
--ring: #94a3b8;
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground font-body;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
margin: 0;
}
::selection {
background: #EFBF04;
color: #03093A;
}
h1, h2, h3, h4, h5, h6 {
@apply font-display font-bold;
}
blockquote, q,
.quote, [class*="italic"] {
font-family: "Inter", sans-serif;
font-style: italic;
}
label,
.label {
font-family: "IBM Plex Sans", sans-serif;
}
input, textarea, select, button {
font-family: "Inter", sans-serif;
}
}
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@layer utilities {
.animate-fade-in {
animation: fade-in 0.6s ease-out;
}
}

View File

@@ -0,0 +1,21 @@
import type { Metadata } from 'next'
import React from 'react'
import './globals.css'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="de">
<head>
<link href="/favicon.ico" rel="icon" sizes="32x32" />
<link href="/favicon.svg" rel="icon" type="image/svg+xml" />
</head>
<body>{children}</body>
</html>
)
}
export const metadata: Metadata = {
title: 'Bayerischer Mittelstandspreis 2026',
description: 'Der Preis des Bayerischen Mittelstands',
}

View File

@@ -0,0 +1,7 @@
import { draftMode } from 'next/headers'
export async function GET(): Promise<Response> {
const draft = await draftMode()
draft.disable()
return new Response('Draft mode is disabled')
}

View File

@@ -0,0 +1,59 @@
import type { PayloadRequest } from 'payload'
import { getPayload } from 'payload'
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
import { NextRequest } from 'next/server'
import configPromise from '@payload-config'
export type PreviewSearchParams = {
path: string
previewSecret: string
}
export async function GET(req: NextRequest): Promise<Response> {
const payload = await getPayload({ config: configPromise })
const { searchParams } = new URL(req.url)
const path = searchParams.get('path')
const previewSecret = searchParams.get('previewSecret')
if (previewSecret !== process.env.PREVIEW_SECRET) {
return new Response('You are not allowed to preview this page', { status: 403 })
}
if (!path) {
return new Response('Insufficient search params', { status: 404 })
}
if (!path.startsWith('/')) {
return new Response('This endpoint can only be used for relative previews', { status: 500 })
}
let user
try {
user = await payload.auth({
req: req as unknown as PayloadRequest,
headers: req.headers,
})
} catch (error) {
payload.logger.error({ err: error }, 'Error verifying token for live preview')
return new Response('You are not allowed to preview this page', { status: 403 })
}
const draft = await draftMode()
if (!user) {
draft.disable()
return new Response('You are not allowed to preview this page', { status: 403 })
}
// You can add additional checks here to see if the user is allowed to preview this page
draft.enable()
redirect(path)
}

View File

@@ -0,0 +1,31 @@
import { createLocalReq, getPayload } from 'payload'
import { seed } from '@/endpoints/seed'
import config from '@payload-config'
import { headers } from 'next/headers'
export const maxDuration = 60 // This function can run for a maximum of 60 seconds
export async function POST(): Promise<Response> {
const payload = await getPayload({ config })
const requestHeaders = await headers()
// Authenticate by passing request headers
const { user } = await payload.auth({ headers: requestHeaders })
if (!user) {
return new Response('Action forbidden.', { status: 403 })
}
try {
// Create a Payload request object to pass to the Local API for transactions
// At this point you should pass in a user, locale, and any other context you need for the Local API
const payloadReq = await createLocalReq({ user }, payload)
await seed({ payload, req: payloadReq })
return Response.json({ success: true })
} catch (e) {
payload.logger.error({ err: e, message: 'Error seeding data' })
return new Response('Error seeding data.', { status: 500 })
}
}

View File

@@ -0,0 +1,18 @@
import Link from 'next/link'
import React from 'react'
import { Button } from '@/components/ui/button'
export default function NotFound() {
return (
<div className="container py-28">
<div className="prose max-w-none">
<h1 style={{ marginBottom: 0 }}>404</h1>
<p className="mb-4">This page could not be found.</p>
</div>
<Button asChild variant="default">
<Link href="/">Go home</Link>
</Button>
</div>
)
}

View File

@@ -0,0 +1,24 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from 'next'
import config from '@payload-config'
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
import { importMap } from '../importMap'
type Args = {
params: Promise<{
segments: string[]
}>
searchParams: Promise<{
[key: string]: string | string[]
}>
}
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const NotFound = ({ params, searchParams }: Args) =>
NotFoundPage({ config, params, searchParams, importMap })
export default NotFound

View File

@@ -0,0 +1,24 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from 'next'
import config from '@payload-config'
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
import { importMap } from '../importMap'
type Args = {
params: Promise<{
segments: string[]
}>
searchParams: Promise<{
[key: string]: string | string[]
}>
}
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const Page = ({ params, searchParams }: Args) =>
RootPage({ config, params, searchParams, importMap })
export default Page

View File

@@ -0,0 +1,80 @@
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { FixedToolbarFeatureClient as FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { OverviewComponent as OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
import { MetaTitleComponent as MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
import { MetaImageComponent as MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
import { MetaDescriptionComponent as MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
import { PreviewComponent as PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
import { SlugField as SlugField_2b8867833a34864a02ddf429b0728a40 } from '@payloadcms/next/client'
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { FolderTableCell as FolderTableCell_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
import { FolderField as FolderField_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
import { LinkToDoc as LinkToDoc_aead06e4cbf6b2620c5c51c9ab283634 } from '@payloadcms/plugin-search/client'
import { ReindexButton as ReindexButton_aead06e4cbf6b2620c5c51c9ab283634 } from '@payloadcms/plugin-search/client'
import { FolderTypeField as FolderTypeField_2b8867833a34864a02ddf429b0728a40 } from '@payloadcms/next/client'
import { RowLabel as RowLabel_ec255a65fa6fa8d1faeb09cf35284224 } from '@/Header/RowLabel'
import { RowLabel as RowLabel_1f6ff6ff633e3695d348f4f3c58f1466 } from '@/Footer/RowLabel'
import { default as default_1a7510af427896d367a49dbf838d2de6 } from '@/components/BeforeDashboard'
import { default as default_8a7ab0eb7ab5c511aba12e68480bfe5e } from '@/components/BeforeLogin'
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
/** @type import('payload').ImportMap */
export const importMap = {
'@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell':
RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
'@payloadcms/richtext-lexical/rsc#RscEntryLexicalField':
RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
'@payloadcms/richtext-lexical/rsc#LexicalDiffComponent':
LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
'@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient':
InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
'@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient':
FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
'@payloadcms/richtext-lexical/client#HeadingFeatureClient':
HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
'@payloadcms/richtext-lexical/client#ParagraphFeatureClient':
ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
'@payloadcms/richtext-lexical/client#UnderlineFeatureClient':
UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
'@payloadcms/richtext-lexical/client#BoldFeatureClient':
BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
'@payloadcms/richtext-lexical/client#ItalicFeatureClient':
ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
'@payloadcms/richtext-lexical/client#LinkFeatureClient':
LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
'@payloadcms/plugin-seo/client#OverviewComponent':
OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860,
'@payloadcms/plugin-seo/client#MetaTitleComponent':
MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860,
'@payloadcms/plugin-seo/client#MetaImageComponent':
MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860,
'@payloadcms/plugin-seo/client#MetaDescriptionComponent':
MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860,
'@payloadcms/plugin-seo/client#PreviewComponent':
PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860,
'@payloadcms/next/client#SlugField': SlugField_2b8867833a34864a02ddf429b0728a40,
'@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient':
HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
'@payloadcms/richtext-lexical/client#BlocksFeatureClient':
BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
'@payloadcms/next/rsc#FolderTableCell': FolderTableCell_f9c02e79a4aed9a3924487c0cd4cafb1,
'@payloadcms/next/rsc#FolderField': FolderField_f9c02e79a4aed9a3924487c0cd4cafb1,
'@payloadcms/plugin-search/client#LinkToDoc': LinkToDoc_aead06e4cbf6b2620c5c51c9ab283634,
'@payloadcms/plugin-search/client#ReindexButton': ReindexButton_aead06e4cbf6b2620c5c51c9ab283634,
'@payloadcms/next/client#FolderTypeField': FolderTypeField_2b8867833a34864a02ddf429b0728a40,
'@/Header/RowLabel#RowLabel': RowLabel_ec255a65fa6fa8d1faeb09cf35284224,
'@/Footer/RowLabel#RowLabel': RowLabel_1f6ff6ff633e3695d348f4f3c58f1466,
'@/components/BeforeDashboard#default': default_1a7510af427896d367a49dbf838d2de6,
'@/components/BeforeLogin#default': default_8a7ab0eb7ab5c511aba12e68480bfe5e,
'@payloadcms/next/rsc#CollectionCards': CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1,
}

View File

@@ -0,0 +1,20 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import '@payloadcms/next/css'
import {
REST_DELETE,
REST_GET,
REST_OPTIONS,
REST_PATCH,
REST_POST,
REST_PUT,
} from '@payloadcms/next/routes'
export const GET = REST_GET(config)
export const POST = REST_POST(config)
export const DELETE = REST_DELETE(config)
export const PATCH = REST_PATCH(config)
export const PUT = REST_PUT(config)
export const OPTIONS = REST_OPTIONS(config)

View File

@@ -0,0 +1,7 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import '@payloadcms/next/css'
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
export const GET = GRAPHQL_PLAYGROUND_GET(config)

View File

@@ -0,0 +1,8 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
export const POST = GRAPHQL_POST(config)
export const OPTIONS = REST_OPTIONS(config)

View File

View File

@@ -0,0 +1,31 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import '@payloadcms/next/css'
import type { ServerFunctionClient } from 'payload'
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
import React from 'react'
import { importMap } from './admin/importMap.js'
import './custom.scss'
type Args = {
children: React.ReactNode
}
const serverFunction: ServerFunctionClient = async function (args) {
'use server'
return handleServerFunctions({
...args,
config,
importMap,
})
}
const Layout = ({ children }: Args) => (
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
{children}
</RootLayout>
)
export default Layout

View File

@@ -0,0 +1,65 @@
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>
)
}

View File

@@ -0,0 +1,94 @@
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',
},
}

View File

@@ -0,0 +1,26 @@
import type { BannerBlock as BannerBlockProps } from 'src/payload-types'
import { cn } from '@/utilities/ui'
import React from 'react'
import RichText from '@/components/RichText'
type Props = {
className?: string
} & BannerBlockProps
export const BannerBlock: React.FC<Props> = ({ className, content, style }) => {
return (
<div className={cn('mx-auto my-8 w-full', className)}>
<div
className={cn('border py-3 px-6 flex items-center rounded', {
'border-border bg-card': style === 'info',
'border-error bg-error/30': style === 'error',
'border-success bg-success/30': style === 'success',
'border-warning bg-warning/30': style === 'warning',
})}
>
<RichText data={content} enableGutter={false} enableProse={false} />
</div>
</div>
)
}

View File

@@ -0,0 +1,37 @@
import type { Block } from 'payload'
import {
FixedToolbarFeature,
InlineToolbarFeature,
lexicalEditor,
} from '@payloadcms/richtext-lexical'
export const Banner: Block = {
slug: 'banner',
fields: [
{
name: 'style',
type: 'select',
defaultValue: 'info',
options: [
{ label: 'Info', value: 'info' },
{ label: 'Warning', value: 'warning' },
{ label: 'Error', value: 'error' },
{ label: 'Success', value: 'success' },
],
required: true,
},
{
name: 'content',
type: 'richText',
editor: lexicalEditor({
features: ({ rootFeatures }) => {
return [...rootFeatures, FixedToolbarFeature(), InlineToolbarFeature()]
},
}),
label: false,
required: true,
},
],
interfaceName: 'BannerBlock',
}

View File

@@ -0,0 +1,23 @@
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>
)
}

View File

@@ -0,0 +1,42 @@
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',
},
}

View File

@@ -0,0 +1,33 @@
'use client'
import { Highlight, themes } from 'prism-react-renderer'
import React from 'react'
import { CopyButton } from './CopyButton'
type Props = {
code: string
language?: string
}
export const Code: React.FC<Props> = ({ code, language = '' }) => {
if (!code) return null
return (
<Highlight code={code} language={language} theme={themes.vsDark}>
{({ getLineProps, getTokenProps, tokens }) => (
<pre className="bg-black p-4 border text-xs border-border rounded overflow-x-auto">
{tokens.map((line, i) => (
<div key={i} {...getLineProps({ className: 'table-row', line })}>
<span className="table-cell select-none text-right text-white/25">{i + 1}</span>
<span className="table-cell pl-4">
{line.map((token, key) => (
<span key={key} {...getTokenProps({ token })} />
))}
</span>
</div>
))}
<CopyButton code={code} />
</pre>
)}
</Highlight>
)
}

View File

@@ -0,0 +1,21 @@
import React from 'react'
import { Code } from './Component.client'
export type CodeBlockProps = {
code: string
language?: string
blockType: 'code'
}
type Props = CodeBlockProps & {
className?: string
}
export const CodeBlock: React.FC<Props> = ({ className, code, language }) => {
return (
<div className={[className, 'not-prose'].filter(Boolean).join(' ')}>
<Code code={code} language={language} />
</div>
)
}

View File

@@ -0,0 +1,33 @@
'use client'
import { Button } from '@/components/ui/button'
import { CopyIcon } from '@payloadcms/ui/icons/Copy'
import { useState } from 'react'
export function CopyButton({ code }: { code: string }) {
const [text, setText] = useState('Copy')
function updateCopyStatus() {
if (text === 'Copy') {
setText(() => 'Copied!')
setTimeout(() => {
setText(() => 'Copy')
}, 1000)
}
}
return (
<div className="flex justify-end align-middle">
<Button
className="flex gap-1"
variant={'secondary'}
onClick={async () => {
await navigator.clipboard.writeText(code)
updateCopyStatus()
}}
>
<p>{text}</p>
<CopyIcon />
</Button>
</div>
)
}

33
src/blocks/Code/config.ts Normal file
View File

@@ -0,0 +1,33 @@
import type { Block } from 'payload'
export const Code: Block = {
slug: 'code',
interfaceName: 'CodeBlock',
fields: [
{
name: 'language',
type: 'select',
defaultValue: 'typescript',
options: [
{
label: 'Typescript',
value: 'typescript',
},
{
label: 'Javascript',
value: 'javascript',
},
{
label: 'CSS',
value: 'css',
},
],
},
{
name: 'code',
type: 'code',
label: false,
required: true,
},
],
}

View File

@@ -0,0 +1,43 @@
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>
)
}

View File

@@ -0,0 +1,79 @@
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,
},
],
}

View File

@@ -0,0 +1,45 @@
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>
)
}

View File

@@ -0,0 +1,163 @@
'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 { DefaultTypedEditorState } from '@payloadcms/richtext-lexical'
import { fields } from './fields'
import { getClientSideURL } from '@/utilities/getURL'
export type FormBlockType = {
blockName?: string
blockType?: 'formBlock'
enableIntro: boolean
form: FormType
introContent?: DefaultTypedEditorState
}
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) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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>
)
}

View File

@@ -0,0 +1,65 @@
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>
)
}

View File

@@ -0,0 +1,982 @@
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',
},
]

View File

@@ -0,0 +1,38 @@
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>
)
}

View File

@@ -0,0 +1,15 @@
'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>
)
}

View File

@@ -0,0 +1,13 @@
import RichText from '@/components/RichText'
import React from 'react'
import { Width } from '../Width'
import { DefaultTypedEditorState } from '@payloadcms/richtext-lexical'
export const Message: React.FC<{ message: DefaultTypedEditorState }> = ({ message }) => {
return (
<Width className="my-12" width="100">
{message && <RichText data={message} />}
</Width>
)
}

View File

@@ -0,0 +1,36 @@
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>
)
}

View File

@@ -0,0 +1,63 @@
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>
)
}

View File

@@ -0,0 +1,64 @@
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>
)
}

View File

@@ -0,0 +1,52 @@
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' },
]

View File

@@ -0,0 +1,32 @@
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>
)
}

View File

@@ -0,0 +1,40 @@
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>
)
}

Some files were not shown because too many files have changed in this diff Show More