2 Achegas ac0af1ac11 ... 0a5c6226ca

Autor SHA1 Mensaxe Data
  YusufSyam 0a5c6226ca feat: added gallery collections hai 2 semanas
  YusufSyam b16a27e935 fix: error author didnt included in posts response hai 2 semanas

+ 0 - 519
.cursor/rules/access-control-advanced.md

@@ -1,519 +0,0 @@
----
-title: Access Control - Advanced Patterns
-description: Context-aware, time-based, subscription-based access, factory functions, templates
-tags: [payload, access-control, security, advanced, performance]
-priority: high
----
-
-# Advanced Access Control Patterns
-
-Advanced access control patterns including context-aware access, time-based restrictions, factory functions, and production templates.
-
-## Context-Aware Access Patterns
-
-### Locale-Specific Access
-
-```typescript
-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
-}
-```
-
-### Device-Specific Access
-
-```typescript
-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)
-}
-```
-
-### IP-Based Access
-
-```typescript
-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),
-  },
-}
-```
-
-## Time-Based Access Patterns
-
-### Today's Records Only
-
-```typescript
-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(),
-    },
-  }
-}
-```
-
-### Recent Records (Last N Days)
-
-```typescript
-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),
-  },
-}
-```
-
-### Scheduled Content (Publish Date Range)
-
-```typescript
-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 } }],
-      },
-    ],
-  }
-}
-```
-
-## Subscription-Based Access
-
-### Active Subscription Required
-
-```typescript
-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
-  }
-}
-```
-
-### Subscription Tier-Based Access
-
-```typescript
-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'),
-  },
-}
-```
-
-## Factory Functions
-
-### createRoleBasedAccess
-
-```typescript
-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'])
-```
-
-### createOrgScopedAccess
-
-```typescript
-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
-```
-
-### createTeamBasedAccess
-
-```typescript
-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 || [] },
-    }
-  }
-}
-```
-
-### createTimeLimitedAccess
-
-```typescript
-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(),
-      },
-    }
-  }
-}
-```
-
-## Configuration Templates
-
-### Public + Authenticated Collection
-
-```typescript
-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' },
-  ],
-}
-```
-
-### Self-Service Collection
-
-```typescript
-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,
-      },
-    },
-  ],
-}
-```
-
-## Performance Considerations
-
-### Avoid Async Operations in Hot Paths
-
-```typescript
-// ❌ 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
-
-```typescript
-// ❌ 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
-
-```typescript
-// ❌ Slow: Complex access on array fields
-{
-  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
-{
-  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
-
-```typescript
-// ❌ 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 } }
-}
-```
-
-## Debugging Tips
-
-### Log Access Check Execution
-
-```typescript
-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
-
-```typescript
-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
-}
-```
-
-### Test Access Without User
-
-```typescript
-// 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)
-```
-
-## Best Practices
-
-1. **Default Deny**: Start with restrictive access, gradually add permissions
-2. **Type Guards**: Use TypeScript for user type safety
-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. **Handle Errors Gracefully**: Access functions should return `false` on error, not throw
-13. **Test Local API**: Remember to set `overrideAccess: false` when testing
-14. **Consider Performance**: Measure impact of async operations
-15. **Principle of Least Privilege**: Grant minimum access required
-
-## Performance Summary
-
-**Minimize Async Operations**: Use query constraints over async lookups when possible
-
-**Cache Expensive Checks**: Store results in `req.context` for reuse
-
-**Index Query Fields**: Ensure fields in query constraints are indexed
-
-**Avoid Complex Logic in Array Fields**: Simple boolean checks preferred
-
-**Use Query Constraints**: Let database filter rather than loading all records

+ 0 - 225
.cursor/rules/access-control.md

@@ -1,225 +0,0 @@
----
-title: Access Control
-description: Collection, field, and global access control patterns
-tags: [payload, access-control, security, permissions, rbac]
----
-
-# Payload CMS Access Control
-
-## Access Control Layers
-
-1. **Collection-Level**: Controls operations on entire documents (create, read, update, delete, admin)
-2. **Field-Level**: Controls access to individual fields (create, read, update)
-3. **Global-Level**: Controls access to global documents (read, update)
-
-## Collection Access Control
-
-```typescript
-import type { 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
-    },
-  },
-}
-```
-
-## Common Access Patterns
-
-```typescript
-// Anyone
-export const anyone: Access = () => true
-
-// Authenticated only
-export const authenticated: Access = ({ req: { user } }) => Boolean(user)
-
-// Admin only
-export const adminOnly: Access = ({ req: { user } }) => {
-  return user?.roles?.includes('admin')
-}
-
-// Admin or self
-export const adminOrSelf: Access = ({ req: { user } }) => {
-  if (user?.roles?.includes('admin')) return true
-  return { id: { equals: user?.id } }
-}
-
-// Published or authenticated
-export const authenticatedOrPublished: Access = ({ req: { user } }) => {
-  if (user) return true
-  return { _status: { equals: 'published' } }
-}
-```
-
-## Row-Level Security
-
-```typescript
-// 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,
-    },
-  }
-}
-
-// 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,
-    },
-  }
-}
-```
-
-## Field Access Control
-
-**Field access ONLY returns boolean** (no query constraints).
-
-```typescript
-{
-  name: 'salary',
-  type: 'number',
-  access: {
-    read: ({ req: { user }, doc }) => {
-      // Self can read own salary
-      if (user?.id === doc?.id) return true
-      // Admin can read all
-      return user?.roles?.includes('admin')
-    },
-    update: ({ req: { user } }) => {
-      // Only admins can update
-      return user?.roles?.includes('admin')
-    },
-  },
-}
-```
-
-## RBAC Pattern
-
-Payload does NOT provide a roles system by default. Add a `roles` field to your auth collection:
-
-```typescript
-export const Users: CollectionConfig = {
-  slug: 'users',
-  auth: true,
-  fields: [
-    {
-      name: 'roles',
-      type: 'select',
-      hasMany: true,
-      options: ['admin', 'editor', 'user'],
-      defaultValue: ['user'],
-      required: true,
-      saveToJWT: true, // Include in JWT for fast access checks
-      access: {
-        update: ({ req: { user } }) => user?.roles?.includes('admin'),
-      },
-    },
-  ],
-}
-```
-
-## Multi-Tenant Access Control
-
-```typescript
-interface User {
-  id: string
-  tenantId: string
-  roles?: string[]
-}
-
-const tenantAccess: Access = ({ req: { user } }) => {
-  if (!user) return false
-  if (user.roles?.includes('super-admin')) return true
-
-  return {
-    tenant: {
-      equals: (user as User).tenantId,
-    },
-  }
-}
-
-export const Posts: CollectionConfig = {
-  slug: 'posts',
-  access: {
-    create: tenantAccess,
-    read: tenantAccess,
-    update: tenantAccess,
-    delete: tenantAccess,
-  },
-  fields: [
-    {
-      name: 'tenant',
-      type: 'text',
-      required: true,
-      access: {
-        update: ({ req: { user } }) => user?.roles?.includes('super-admin'),
-      },
-      hooks: {
-        beforeChange: [
-          ({ req, operation, value }) => {
-            if (operation === 'create' && !value) {
-              return (req.user as User)?.tenantId
-            }
-            return value
-          },
-        ],
-      },
-    },
-  ],
-}
-```
-
-## Important Notes
-
-1. **Local API Default**: Access control is **skipped by default** in Local API (`overrideAccess: true`). When passing a `user` parameter, you must set `overrideAccess: false`:
-
-```typescript
-// ❌ WRONG: Passes user but bypasses access control
-await payload.find({
-  collection: 'posts',
-  user: someUser,
-})
-
-// ✅ CORRECT: Respects the user's permissions
-await payload.find({
-  collection: 'posts',
-  user: someUser,
-  overrideAccess: false, // Required to enforce access control
-})
-```
-
-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.

+ 0 - 209
.cursor/rules/adapters.md

@@ -1,209 +0,0 @@
----
-title: Database Adapters & Transactions
-description: Database adapters, storage, email, and transaction patterns
-tags: [payload, database, mongodb, postgres, sqlite, transactions]
----
-
-# Payload CMS Adapters
-
-## Database Adapters
-
-### MongoDB
-
-```typescript
-import { mongooseAdapter } from '@payloadcms/db-mongodb'
-
-export default buildConfig({
-  db: mongooseAdapter({
-    url: process.env.DATABASE_URL,
-  }),
-})
-```
-
-### Postgres
-
-```typescript
-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
-
-```typescript
-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.
-
-### Threading req Through Operations
-
-**CRITICAL**: When performing nested operations in hooks, always pass `req` to maintain transaction context.
-
-```typescript
-// ✅ 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
-
-### Manual Transaction Control
-
-```typescript
-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
-}
-```
-
-## 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
-
-```typescript
-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,
-      },
-    }),
-  ],
-})
-```
-
-## Email Adapters
-
-### Nodemailer (SMTP)
-
-```typescript
-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
-
-```typescript
-import { resendAdapter } from '@payloadcms/email-resend'
-
-export default buildConfig({
-  email: resendAdapter({
-    defaultFromAddress: 'noreply@example.com',
-    defaultFromName: 'My App',
-    apiKey: process.env.RESEND_API_KEY,
-  }),
-})
-```
-
-## Important Notes
-
-1. **MongoDB Transactions**: Require replica set configuration
-2. **SQLite Transactions**: Disabled by default, enable with `transactionOptions: {}`
-3. **Pass req**: Always pass `req` to nested operations in hooks for transaction safety
-4. **Point Fields**: Not supported in SQLite

+ 0 - 171
.cursor/rules/collections.md

@@ -1,171 +0,0 @@
----
-title: Collections
-description: Collection configurations and patterns
-tags: [payload, collections, auth, upload, drafts]
----
-
-# Payload CMS Collections
-
-## Basic Collection
-
-```typescript
-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,
-}
-```
-
-## Auth Collection with RBAC
-
-```typescript
-export const Users: CollectionConfig = {
-  slug: 'users',
-  auth: true,
-  fields: [
-    {
-      name: 'roles',
-      type: 'select',
-      hasMany: true,
-      options: ['admin', 'editor', 'user'],
-      defaultValue: ['user'],
-      required: true,
-      saveToJWT: true, // Include in JWT for fast access checks
-      access: {
-        update: ({ req: { user } }) => user?.roles?.includes('admin'),
-      },
-    },
-  ],
-}
-```
-
-## Upload Collection
-
-```typescript
-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,
-    },
-  ],
-}
-```
-
-## Versioning & Drafts
-
-```typescript
-export const Pages: CollectionConfig = {
-  slug: 'pages',
-  versions: {
-    drafts: {
-      autosave: true,
-      schedulePublish: true,
-      validate: false, // Don't validate drafts
-    },
-    maxPerDoc: 100,
-  },
-  access: {
-    read: ({ req: { user } }) => {
-      // Public sees only published
-      if (!user) return { _status: { equals: 'published' } }
-      // Authenticated sees all
-      return true
-    },
-  },
-}
-```
-
-### Draft API Usage
-
-```typescript
-// Create draft
-await payload.create({
-  collection: 'posts',
-  data: { title: 'Draft Post' },
-  draft: true, // Skips required field validation
-})
-
-// Read with drafts
-const page = await payload.findByID({
-  collection: 'pages',
-  id: '123',
-  draft: true, // Returns draft version if exists
-})
-```
-
-## Globals
-
-Globals are single-instance documents (not collections).
-
-```typescript
-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',
-        },
-      ],
-    },
-  ],
-}
-```

+ 0 - 794
.cursor/rules/components.md

@@ -1,794 +0,0 @@
-# Custom Components in Payload CMS
-
-Custom Components allow you to fully customize the Admin Panel by swapping in your own React components. You can replace nearly every part of the interface or add entirely new functionality.
-
-## Component Types
-
-There are four main types of Custom Components:
-
-1. **Root Components** - Affect the Admin Panel globally (logo, nav, header)
-2. **Collection Components** - Specific to collection views
-3. **Global Components** - Specific to global document views
-4. **Field Components** - Custom field UI and cells
-
-## Defining Custom Components
-
-### Component Paths
-
-Components are defined using file paths (not direct imports) to keep the config lightweight and Node.js compatible.
-
-```typescript
-import { buildConfig } from 'payload'
-
-export default buildConfig({
-  admin: {
-    components: {
-      logout: {
-        Button: '/src/components/Logout#MyComponent', // Named export
-      },
-      Nav: '/src/components/Nav', // Default export
-    },
-  },
-})
-```
-
-**Component Path Rules:**
-
-1. Paths are relative to project root (or `config.admin.importMap.baseDir`)
-2. For **named exports**: append `#ExportName` or use `exportName` property
-3. For **default exports**: no suffix needed
-4. File extensions can be omitted
-
-### Component Config Object
-
-Instead of a string path, you can pass a config object:
-
-```typescript
-{
-  logout: {
-    Button: {
-      path: '/src/components/Logout',
-      exportName: 'MyComponent',
-      clientProps: { customProp: 'value' },
-      serverProps: { asyncData: someData },
-    },
-  },
-}
-```
-
-**Config Properties:**
-
-| Property      | Description                                           |
-| ------------- | ----------------------------------------------------- |
-| `path`        | File path to component (named exports via `#`)        |
-| `exportName`  | Named export (alternative to `#` in path)             |
-| `clientProps` | Props for Client Components (must be serializable)    |
-| `serverProps` | Props for Server Components (can be non-serializable) |
-
-### Setting Base Directory
-
-```typescript
-import path from 'path'
-import { fileURLToPath } from 'node:url'
-
-const filename = fileURLToPath(import.meta.url)
-const dirname = path.dirname(filename)
-
-export default buildConfig({
-  admin: {
-    importMap: {
-      baseDir: path.resolve(dirname, 'src'), // Set base directory
-    },
-    components: {
-      Nav: '/components/Nav', // Now relative to src/
-    },
-  },
-})
-```
-
-## Server vs Client Components
-
-**All components are React Server Components by default.**
-
-### Server Components (Default)
-
-Can use Local API directly, perform async operations, and access full Payload instance.
-
-```tsx
-import React from 'react'
-import type { Payload } from 'payload'
-
-async function MyServerComponent({ payload }: { payload: Payload }) {
-  const page = await payload.findByID({
-    collection: 'pages',
-    id: '123',
-  })
-
-  return <p>{page.title}</p>
-}
-
-export default MyServerComponent
-```
-
-### Client Components
-
-Use the `'use client'` directive for interactivity, hooks, state, etc.
-
-```tsx
-'use client'
-import React, { useState } from 'react'
-
-export function MyClientComponent() {
-  const [count, setCount] = useState(0)
-
-  return <button onClick={() => setCount(count + 1)}>Clicked {count} times</button>
-}
-```
-
-**Important:** Client Components cannot receive non-serializable props (functions, class instances, etc.). Payload automatically strips these when passing to client components.
-
-## Default Props
-
-All Custom Components receive these props by default:
-
-| Prop      | Description                              | Type      |
-| --------- | ---------------------------------------- | --------- |
-| `payload` | Payload instance (Local API access)      | `Payload` |
-| `i18n`    | Internationalization object              | `I18n`    |
-| `locale`  | Current locale (if localization enabled) | `string`  |
-
-**Server Component Example:**
-
-```tsx
-async function MyComponent({ payload, i18n, locale }) {
-  const data = await payload.find({
-    collection: 'posts',
-    locale,
-  })
-
-  return <div>{data.docs.length} posts</div>
-}
-```
-
-**Client Component Example:**
-
-```tsx
-'use client'
-import { usePayload, useLocale, useTranslation } from '@payloadcms/ui'
-
-export function MyComponent() {
-  // Access via hooks in client components
-  const { getLocal, getByID } = usePayload()
-  const locale = useLocale()
-  const { t, i18n } = useTranslation()
-
-  return <div>{t('myKey')}</div>
-}
-```
-
-## Custom Props
-
-Pass additional props using `clientProps` or `serverProps`:
-
-```typescript
-{
-  logout: {
-    Button: {
-      path: '/components/Logout',
-      clientProps: {
-        buttonText: 'Sign Out',
-        onLogout: () => console.log('Logged out'),
-      },
-    },
-  },
-}
-```
-
-Receive in component:
-
-```tsx
-'use client'
-export function Logout({ buttonText, onLogout }) {
-  return <button onClick={onLogout}>{buttonText}</button>
-}
-```
-
-## Root Components
-
-Root Components affect the entire Admin Panel.
-
-### Available Root Components
-
-| Component         | Description                      | Config Path                        |
-| ----------------- | -------------------------------- | ---------------------------------- |
-| `Nav`             | Entire navigation sidebar        | `admin.components.Nav`             |
-| `graphics.Icon`   | Small icon (used in nav)         | `admin.components.graphics.Icon`   |
-| `graphics.Logo`   | Full logo (used on login)        | `admin.components.graphics.Logo`   |
-| `logout.Button`   | Logout button                    | `admin.components.logout.Button`   |
-| `actions`         | Header actions (array)           | `admin.components.actions`         |
-| `header`          | Above header (array)             | `admin.components.header`          |
-| `beforeDashboard` | Before dashboard content (array) | `admin.components.beforeDashboard` |
-| `afterDashboard`  | After dashboard content (array)  | `admin.components.afterDashboard`  |
-| `beforeLogin`     | Before login form (array)        | `admin.components.beforeLogin`     |
-| `afterLogin`      | After login form (array)         | `admin.components.afterLogin`      |
-| `beforeNavLinks`  | Before nav links (array)         | `admin.components.beforeNavLinks`  |
-| `afterNavLinks`   | After nav links (array)          | `admin.components.afterNavLinks`   |
-| `settingsMenu`    | Settings menu items (array)      | `admin.components.settingsMenu`    |
-| `providers`       | Custom React Context providers   | `admin.components.providers`       |
-| `views`           | Custom views (dashboard, etc.)   | `admin.components.views`           |
-
-### Example: Custom Logo
-
-```typescript
-export default buildConfig({
-  admin: {
-    components: {
-      graphics: {
-        Logo: '/components/Logo',
-        Icon: '/components/Icon',
-      },
-    },
-  },
-})
-```
-
-```tsx
-// components/Logo.tsx
-export default function Logo() {
-  return <img src="/logo.png" alt="My Brand" width={200} />
-}
-```
-
-### Example: Header Actions
-
-```typescript
-export default buildConfig({
-  admin: {
-    components: {
-      actions: ['/components/ClearCacheButton', '/components/PreviewButton'],
-    },
-  },
-})
-```
-
-```tsx
-// components/ClearCacheButton.tsx
-'use client'
-export default function ClearCacheButton() {
-  return (
-    <button
-      onClick={async () => {
-        await fetch('/api/clear-cache', { method: 'POST' })
-        alert('Cache cleared!')
-      }}
-    >
-      Clear Cache
-    </button>
-  )
-}
-```
-
-## Collection Components
-
-Collection Components are specific to a collection's views.
-
-```typescript
-import type { CollectionConfig } from 'payload'
-
-export const Posts: CollectionConfig = {
-  slug: 'posts',
-  admin: {
-    components: {
-      // Edit view components
-      edit: {
-        PreviewButton: '/components/PostPreview',
-        SaveButton: '/components/CustomSave',
-        SaveDraftButton: '/components/CustomSaveDraft',
-        PublishButton: '/components/CustomPublish',
-      },
-
-      // List view components
-      list: {
-        Header: '/components/PostsListHeader',
-        beforeList: ['/components/ListFilters'],
-        afterList: ['/components/ListFooter'],
-      },
-    },
-  },
-  fields: [
-    // ...
-  ],
-}
-```
-
-## Global Components
-
-Similar to Collection Components but for Global documents.
-
-```typescript
-import type { GlobalConfig } from 'payload'
-
-export const Settings: GlobalConfig = {
-  slug: 'settings',
-  admin: {
-    components: {
-      edit: {
-        PreviewButton: '/components/SettingsPreview',
-        SaveButton: '/components/SettingsSave',
-      },
-    },
-  },
-  fields: [
-    // ...
-  ],
-}
-```
-
-## Field Components
-
-Customize how fields render in Edit and List views.
-
-### Field Component (Edit View)
-
-```typescript
-{
-  name: 'status',
-  type: 'select',
-  options: ['draft', 'published'],
-  admin: {
-    components: {
-      Field: '/components/StatusField',
-    },
-  },
-}
-```
-
-```tsx
-// components/StatusField.tsx
-'use client'
-import { useField } from '@payloadcms/ui'
-import type { SelectFieldClientComponent } from 'payload'
-
-export const StatusField: SelectFieldClientComponent = ({ path, field }) => {
-  const { value, setValue } = useField({ path })
-
-  return (
-    <div>
-      <label>{field.label}</label>
-      <select value={value} onChange={(e) => setValue(e.target.value)}>
-        {field.options.map((option) => (
-          <option key={option.value} value={option.value}>
-            {option.label}
-          </option>
-        ))}
-      </select>
-    </div>
-  )
-}
-```
-
-### Cell Component (List View)
-
-```typescript
-{
-  name: 'status',
-  type: 'select',
-  options: ['draft', 'published'],
-  admin: {
-    components: {
-      Cell: '/components/StatusCell',
-    },
-  },
-}
-```
-
-```tsx
-// components/StatusCell.tsx
-import type { SelectFieldCellComponent } from 'payload'
-
-export const StatusCell: SelectFieldCellComponent = ({ data, cellData }) => {
-  const isPublished = cellData === 'published'
-
-  return (
-    <span
-      style={{
-        color: isPublished ? 'green' : 'orange',
-        fontWeight: 'bold',
-      }}
-    >
-      {cellData}
-    </span>
-  )
-}
-```
-
-### UI Field (Presentational Only)
-
-Special field type for adding custom UI without affecting data:
-
-```typescript
-{
-  name: 'refundButton',
-  type: 'ui',
-  admin: {
-    components: {
-      Field: '/components/RefundButton',
-    },
-  },
-}
-```
-
-```tsx
-// components/RefundButton.tsx
-'use client'
-import { useDocumentInfo } from '@payloadcms/ui'
-
-export default function RefundButton() {
-  const { id } = useDocumentInfo()
-
-  return (
-    <button
-      onClick={async () => {
-        await fetch(`/api/orders/${id}/refund`, { method: 'POST' })
-        alert('Refund processed')
-      }}
-    >
-      Process Refund
-    </button>
-  )
-}
-```
-
-## Using Hooks
-
-Payload provides many React hooks for Client Components:
-
-```tsx
-'use client'
-import {
-  useAuth, // Current user
-  useConfig, // Payload config (client-safe)
-  useDocumentInfo, // Current document info (id, slug, etc.)
-  useField, // Field value and setValue
-  useForm, // Form state and dispatch
-  useFormFields, // Multiple field values (optimized)
-  useLocale, // Current locale
-  useTranslation, // i18n translations
-  usePayload, // Local API methods
-} from '@payloadcms/ui'
-
-export function MyComponent() {
-  const { user } = useAuth()
-  const { config } = useConfig()
-  const { id, collection } = useDocumentInfo()
-  const locale = useLocale()
-  const { t } = useTranslation()
-
-  return <div>Hello {user?.email}</div>
-}
-```
-
-**Important:** These hooks only work in Client Components within the Admin Panel context.
-
-## Accessing Payload Config
-
-**In Server Components:**
-
-```tsx
-async function MyServerComponent({ payload }) {
-  const { config } = payload
-  return <div>{config.serverURL}</div>
-}
-```
-
-**In Client Components:**
-
-```tsx
-'use client'
-import { useConfig } from '@payloadcms/ui'
-
-export function MyClientComponent() {
-  const { config } = useConfig() // Client-safe config
-  return <div>{config.serverURL}</div>
-}
-```
-
-**Important:** Client Components receive a serializable version of the config (functions, validation, etc. are stripped).
-
-## Field Config Access
-
-**Server Component:**
-
-```tsx
-import type { TextFieldServerComponent } from 'payload'
-
-export const MyFieldComponent: TextFieldServerComponent = ({ field }) => {
-  return <div>Field name: {field.name}</div>
-}
-```
-
-**Client Component:**
-
-```tsx
-'use client'
-import type { TextFieldClientComponent } from 'payload'
-
-export const MyFieldComponent: TextFieldClientComponent = ({ clientField }) => {
-  // clientField has non-serializable props removed
-  return <div>Field name: {clientField.name}</div>
-}
-```
-
-## Translations (i18n)
-
-**Server Component:**
-
-```tsx
-import { getTranslation } from '@payloadcms/translations'
-
-async function MyServerComponent({ i18n }) {
-  const translatedTitle = getTranslation(myTranslation, i18n)
-  return <p>{translatedTitle}</p>
-}
-```
-
-**Client Component:**
-
-```tsx
-'use client'
-import { useTranslation } from '@payloadcms/ui'
-
-export function MyClientComponent() {
-  const { t, i18n } = useTranslation()
-
-  return (
-    <div>
-      <p>{t('namespace:key', { variable: 'value' })}</p>
-      <p>Language: {i18n.language}</p>
-    </div>
-  )
-}
-```
-
-## Styling Components
-
-### Using CSS Variables
-
-```tsx
-import './styles.scss'
-
-export function MyComponent() {
-  return <div className="my-component">Custom Component</div>
-}
-```
-
-```scss
-// styles.scss
-.my-component {
-  background-color: var(--theme-elevation-500);
-  color: var(--theme-text);
-  padding: var(--base);
-  border-radius: var(--border-radius-m);
-}
-```
-
-### Importing Payload SCSS
-
-```scss
-@import '~@payloadcms/ui/scss';
-
-.my-component {
-  @include mid-break {
-    background-color: var(--theme-elevation-900);
-  }
-}
-```
-
-## Common Patterns
-
-### Conditional Field Visibility
-
-```tsx
-'use client'
-import { useFormFields } from '@payloadcms/ui'
-import type { TextFieldClientComponent } from 'payload'
-
-export const ConditionalField: TextFieldClientComponent = ({ path }) => {
-  const showField = useFormFields(([fields]) => fields.enableFeature?.value)
-
-  if (!showField) return null
-
-  return <input type="text" />
-}
-```
-
-### Loading Data from API
-
-```tsx
-'use client'
-import { useState, useEffect } from 'react'
-
-export function DataLoader() {
-  const [data, setData] = useState(null)
-
-  useEffect(() => {
-    fetch('/api/custom-data')
-      .then((res) => res.json())
-      .then(setData)
-  }, [])
-
-  return <div>{JSON.stringify(data)}</div>
-}
-```
-
-### Using Local API in Server Components
-
-```tsx
-import type { Payload } from 'payload'
-
-async function RelatedPosts({ payload, id }: { payload: Payload; id: string }) {
-  const post = await payload.findByID({
-    collection: 'posts',
-    id,
-    depth: 0,
-  })
-
-  const related = await payload.find({
-    collection: 'posts',
-    where: {
-      category: { equals: post.category },
-      id: { not_equals: id },
-    },
-    limit: 5,
-  })
-
-  return (
-    <div>
-      <h3>Related Posts</h3>
-      <ul>
-        {related.docs.map((doc) => (
-          <li key={doc.id}>{doc.title}</li>
-        ))}
-      </ul>
-    </div>
-  )
-}
-
-export default RelatedPosts
-```
-
-## Performance Best Practices
-
-### 1. Minimize Client Bundle Size
-
-```tsx
-// ❌ BAD: Imports entire package
-'use client'
-import { Button } from '@payloadcms/ui'
-
-// ✅ GOOD: Tree-shakeable import for frontend
-import { Button } from '@payloadcms/ui/elements/Button'
-```
-
-**Rule:** In Admin Panel UI, import from `@payloadcms/ui`. In frontend code, use specific paths.
-
-### 2. Optimize Re-renders
-
-```tsx
-// ❌ BAD: Re-renders on every form change
-'use client'
-import { useForm } from '@payloadcms/ui'
-
-export function MyComponent() {
-  const { fields } = useForm()
-  // Re-renders on ANY field change
-}
-
-// ✅ GOOD: Only re-renders when specific field changes
-;('use client')
-import { useFormFields } from '@payloadcms/ui'
-
-export function MyComponent({ path }) {
-  const value = useFormFields(([fields]) => fields[path])
-  // Only re-renders when this field changes
-}
-```
-
-### 3. Use Server Components When Possible
-
-```tsx
-// ✅ GOOD: No JavaScript sent to client
-async function PostCount({ payload }) {
-  const { totalDocs } = await payload.find({
-    collection: 'posts',
-    limit: 0,
-  })
-
-  return <p>{totalDocs} posts</p>
-}
-
-// Only use client components when you need:
-// - State (useState, useReducer)
-// - Effects (useEffect)
-// - Event handlers (onClick, onChange)
-// - Browser APIs (localStorage, window)
-```
-
-### 4. React Best Practices
-
-- Use React.memo() for expensive components
-- Implement proper key props in lists
-- Avoid inline function definitions in renders
-- Use Suspense boundaries for async operations
-
-## Import Map
-
-Payload generates an import map at `app/(payload)/admin/importMap.js` that resolves all component paths.
-
-**Regenerate manually:**
-
-```bash
-payload generate:importmap
-```
-
-**Override location:**
-
-```typescript
-export default buildConfig({
-  admin: {
-    importMap: {
-      baseDir: path.resolve(dirname, 'src'),
-      importMapFile: path.resolve(dirname, 'app', 'custom-import-map.js'),
-    },
-  },
-})
-```
-
-## Type Safety
-
-Use Payload's TypeScript types for components:
-
-```tsx
-import type {
-  TextFieldServerComponent,
-  TextFieldClientComponent,
-  TextFieldCellComponent,
-} from 'payload'
-
-export const MyFieldComponent: TextFieldServerComponent = (props) => {
-  // Fully typed props
-}
-```
-
-## Troubleshooting
-
-### "useConfig is undefined" or similar hook errors
-
-**Cause:** Dependency version mismatch between Payload packages.
-
-**Solution:** Pin all `@payloadcms/*` packages to the exact same version:
-
-```json
-{
-  "dependencies": {
-    "payload": "3.0.0",
-    "@payloadcms/ui": "3.0.0",
-    "@payloadcms/richtext-lexical": "3.0.0"
-  }
-}
-```
-
-### Component not loading
-
-1. Check file path is correct (relative to baseDir)
-2. Verify named export syntax: `/path/to/file#ExportName`
-3. Run `payload generate:importmap` to regenerate
-4. Check for TypeScript errors in component file
-
-## Resources
-
-- [Custom Components Docs](https://payloadcms.com/docs/custom-components/overview)
-- [Root Components](https://payloadcms.com/docs/custom-components/root-components)
-- [Custom Views](https://payloadcms.com/docs/custom-components/custom-views)
-- [React Hooks](https://payloadcms.com/docs/admin/react-hooks)
-- [Custom CSS](https://payloadcms.com/docs/admin/customizing-css)

+ 0 - 236
.cursor/rules/endpoints.md

@@ -1,236 +0,0 @@
----
-title: Custom Endpoints
-description: Custom REST API endpoints with authentication and helpers
-tags: [payload, endpoints, api, routes, webhooks]
----
-
-# Payload Custom Endpoints
-
-## Basic Endpoint Pattern
-
-Custom endpoints are **not authenticated by default**. Always check `req.user`.
-
-```typescript
-import { APIError } from 'payload'
-import type { Endpoint } from 'payload'
-
-export const protectedEndpoint: Endpoint = {
-  path: '/protected',
-  method: 'get',
-  handler: async (req) => {
-    if (!req.user) {
-      throw new APIError('Unauthorized', 401)
-    }
-
-    // Use req.payload for database operations
-    const data = await req.payload.find({
-      collection: 'posts',
-      where: { author: { equals: req.user.id } },
-    })
-
-    return Response.json(data)
-  },
-}
-```
-
-## Route Parameters
-
-```typescript
-export const trackingEndpoint: Endpoint = {
-  path: '/:id/tracking',
-  method: 'get',
-  handler: async (req) => {
-    const { id } = req.routeParams
-
-    const tracking = await getTrackingInfo(id)
-
-    if (!tracking) {
-      return Response.json({ error: 'not found' }, { status: 404 })
-    }
-
-    return Response.json(tracking)
-  },
-}
-```
-
-## Request Body Handling
-
-```typescript
-// Manual JSON parsing
-export const createEndpoint: Endpoint = {
-  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)
-  },
-}
-
-// Using helper (handles JSON + files)
-import { addDataAndFileToRequest } from 'payload'
-
-export const uploadEndpoint: Endpoint = {
-  path: '/upload',
-  method: 'post',
-  handler: async (req) => {
-    await addDataAndFileToRequest(req)
-
-    // req.data 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)
-  },
-}
-```
-
-## Query Parameters
-
-```typescript
-export const searchEndpoint: Endpoint = {
-  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)
-  },
-}
-```
-
-## CORS Headers
-
-```typescript
-import { headersWithCors } from 'payload'
-
-export const corsEndpoint: Endpoint = {
-  path: '/public-data',
-  method: 'get',
-  handler: async (req) => {
-    const data = await fetchPublicData()
-
-    return Response.json(data, {
-      headers: headersWithCors({
-        headers: new Headers(),
-        req,
-      }),
-    })
-  },
-}
-```
-
-## Error Handling
-
-```typescript
-import { APIError } from 'payload'
-
-export const validateEndpoint: Endpoint = {
-  path: '/validate',
-  method: 'post',
-  handler: async (req) => {
-    const data = await req.json()
-
-    if (!data.email) {
-      throw new APIError('Email is required', 400)
-    }
-
-    return Response.json({ valid: true })
-  },
-}
-```
-
-## Endpoint Placement
-
-### Collection Endpoints
-
-Mounted at `/api/{collection-slug}/{path}`.
-
-```typescript
-export const Orders: CollectionConfig = {
-  slug: 'orders',
-  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}`.
-
-```typescript
-export const Settings: GlobalConfig = {
-  slug: 'settings',
-  endpoints: [
-    {
-      path: '/clear-cache',
-      method: 'post',
-      handler: async (req) => {
-        // Available at: /api/globals/settings/clear-cache
-        await clearCache()
-        return Response.json({ message: 'Cache cleared' })
-      },
-    },
-  ],
-}
-```
-
-### Root Endpoints
-
-Mounted at `/api/{path}`.
-
-```typescript
-export default buildConfig({
-  endpoints: [
-    {
-      path: '/hello',
-      method: 'get',
-      handler: () => {
-        // Available at: /api/hello
-        return Response.json({ message: 'Hello!' })
-      },
-    },
-  ],
-})
-```
-
-## 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`
-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. **Log errors** - Use `req.payload.logger` for debugging

+ 0 - 230
.cursor/rules/field-type-guards.md

@@ -1,230 +0,0 @@
----
-title: Field Type Guards
-description: Runtime field type checking and safe type narrowing
-tags: [payload, typescript, type-guards, fields]
----
-
-# Payload Field Type Guards
-
-Type guards for runtime field type checking and safe type narrowing.
-
-## Most Common Guards
-
-### fieldAffectsData
-
-**Most commonly used guard.** Checks if field stores data (has name and is not UI-only).
-
-```typescript
-import { fieldAffectsData } from 'payload'
-
-function generateSchema(fields: Field[]) {
-  fields.forEach((field) => {
-    if (fieldAffectsData(field)) {
-      // Safe to access field.name
-      schema[field.name] = getFieldType(field)
-    }
-  })
-}
-
-// Filter data fields
-const dataFields = fields.filter(fieldAffectsData)
-```
-
-### fieldHasSubFields
-
-Checks if field contains nested fields (group, array, row, or collapsible).
-
-```typescript
-import { fieldHasSubFields } from 'payload'
-
-function traverseFields(fields: Field[]): void {
-  fields.forEach((field) => {
-    if (fieldHasSubFields(field)) {
-      // Safe to access field.fields
-      traverseFields(field.fields)
-    }
-  })
-}
-```
-
-### fieldIsArrayType
-
-Checks if field type is `'array'`.
-
-```typescript
-import { fieldIsArrayType } from 'payload'
-
-if (fieldIsArrayType(field)) {
-  // field.type === 'array'
-  console.log(`Min rows: ${field.minRows}`)
-  console.log(`Max rows: ${field.maxRows}`)
-}
-```
-
-## Capability Guards
-
-### fieldSupportsMany
-
-Checks if field can have multiple values (select, relationship, or upload with `hasMany`).
-
-```typescript
-import { fieldSupportsMany } from 'payload'
-
-if (fieldSupportsMany(field)) {
-  // field.type is 'select' | 'relationship' | 'upload'
-  if (field.hasMany) {
-    console.log('Field accepts multiple values')
-  }
-}
-```
-
-### fieldHasMaxDepth
-
-Checks if field is relationship/upload/join with numeric `maxDepth` property.
-
-```typescript
-import { fieldHasMaxDepth } from 'payload'
-
-if (fieldHasMaxDepth(field)) {
-  // field.type is 'upload' | 'relationship' | 'join'
-  // AND field.maxDepth is number
-  const remainingDepth = field.maxDepth - currentDepth
-}
-```
-
-### fieldIsVirtual
-
-Checks if field is virtual (computed or virtual relationship).
-
-```typescript
-import { fieldIsVirtual } from 'payload'
-
-if (fieldIsVirtual(field)) {
-  // field.virtual is truthy
-  if (typeof field.virtual === 'string') {
-    console.log(`Virtual path: ${field.virtual}`)
-  }
-}
-```
-
-## Type Checking Guards
-
-### fieldIsBlockType
-
-```typescript
-import { fieldIsBlockType } from 'payload'
-
-if (fieldIsBlockType(field)) {
-  // field.type === 'blocks'
-  field.blocks.forEach((block) => {
-    console.log(`Block: ${block.slug}`)
-  })
-}
-```
-
-### fieldIsGroupType
-
-```typescript
-import { fieldIsGroupType } from 'payload'
-
-if (fieldIsGroupType(field)) {
-  // field.type === 'group'
-  console.log(`Interface: ${field.interfaceName}`)
-}
-```
-
-### fieldIsPresentationalOnly
-
-```typescript
-import { fieldIsPresentationalOnly } from 'payload'
-
-if (fieldIsPresentationalOnly(field)) {
-  // field.type === 'ui'
-  // Skip in data operations, GraphQL schema, etc.
-  return
-}
-```
-
-## Common Patterns
-
-### Recursive Field Traversal
-
-```typescript
-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
-
-```typescript
-import { fieldAffectsData, fieldIsPresentationalOnly, fieldIsHiddenOrDisabled } from 'payload'
-
-const dataFields = fields.filter(
-  (field) =>
-    fieldAffectsData(field) && !fieldIsPresentationalOnly(field) && !fieldIsHiddenOrDisabled(field),
-)
-```
-
-### Container Type Switching
-
-```typescript
-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
-
-```typescript
-import { fieldSupportsMany, fieldHasMaxDepth } from 'payload'
-
-// 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
-}
-```
-
-## All Available Guards
-
-| Type Guard                  | Checks For                        | Use When                                 |
-| --------------------------- | --------------------------------- | ---------------------------------------- |
-| `fieldAffectsData`          | Field stores data (has name)      | Need to access field data or name        |
-| `fieldHasSubFields`         | Field contains nested fields      | 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    | Check for `hasMany` support              |
-| `fieldHasMaxDepth`          | Field supports depth control      | Control relationship/upload/join depth   |
-| `fieldIsPresentationalOnly` | Field is UI-only                  | 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          | Proper locale table checks               |
-| `fieldIsVirtual`            | Field is virtual                  | 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}`        | 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         |

+ 0 - 317
.cursor/rules/fields.md

@@ -1,317 +0,0 @@
----
-title: Fields
-description: Field types, patterns, and configurations
-tags: [payload, fields, validation, conditional]
----
-
-# Payload CMS Fields
-
-## Common Field Patterns
-
-```typescript
-// Auto-generate slugs
-import { slugField } from 'payload'
-slugField({ fieldToUse: 'title' })
-
-// Relationship with filtering
-{
-  name: 'category',
-  type: 'relationship',
-  relationTo: 'categories',
-  filterOptions: { active: { equals: true } },
-}
-
-// Conditional field
-{
-  name: 'featuredImage',
-  type: 'upload',
-  relationTo: 'media',
-  admin: {
-    condition: (data) => data.featured === true,
-  },
-}
-
-// Virtual field
-{
-  name: 'fullName',
-  type: 'text',
-  virtual: true,
-  hooks: {
-    afterRead: [({ siblingData }) => `${siblingData.firstName} ${siblingData.lastName}`],
-  },
-}
-```
-
-## Field Types
-
-### Text Field
-
-```typescript
-{
-  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,
-  },
-}
-```
-
-### Rich Text (Lexical)
-
-```typescript
-import { lexicalEditor } from '@payloadcms/richtext-lexical'
-import { HeadingFeature, LinkFeature } from '@payloadcms/richtext-lexical'
-
-{
-  name: 'content',
-  type: 'richText',
-  required: true,
-  editor: lexicalEditor({
-    features: ({ defaultFeatures }) => [
-      ...defaultFeatures,
-      HeadingFeature({
-        enabledHeadingSizes: ['h1', 'h2', 'h3'],
-      }),
-      LinkFeature({
-        enabledCollections: ['posts', 'pages'],
-      }),
-    ],
-  }),
-}
-```
-
-### Relationship
-
-```typescript
-// Single relationship
-{
-  name: 'author',
-  type: 'relationship',
-  relationTo: 'users',
-  required: true,
-  maxDepth: 2,
-}
-
-// Multiple relationships (hasMany)
-{
-  name: 'categories',
-  type: 'relationship',
-  relationTo: 'categories',
-  hasMany: true,
-  filterOptions: {
-    active: { equals: true },
-  },
-}
-
-// Polymorphic relationship
-{
-  name: 'relatedContent',
-  type: 'relationship',
-  relationTo: ['posts', 'pages'],
-  hasMany: true,
-}
-```
-
-### Array
-
-```typescript
-{
-  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
-
-```typescript
-import type { 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',
-    },
-  ],
-}
-
-{
-  name: 'layout',
-  type: 'blocks',
-  blocks: [HeroBlock, ContentBlock],
-}
-```
-
-### Select
-
-```typescript
-{
-  name: 'status',
-  type: 'select',
-  options: [
-    { label: 'Draft', value: 'draft' },
-    { label: 'Published', value: 'published' },
-  ],
-  defaultValue: 'draft',
-  required: true,
-}
-
-// Multiple select
-{
-  name: 'tags',
-  type: 'select',
-  hasMany: true,
-  options: ['tech', 'news', 'sports'],
-}
-```
-
-### Upload
-
-```typescript
-{
-  name: 'featuredImage',
-  type: 'upload',
-  relationTo: 'media',
-  required: true,
-  filterOptions: {
-    mimeType: { contains: 'image' },
-  },
-}
-```
-
-### Point (Geolocation)
-
-```typescript
-{
-  name: 'location',
-  type: 'point',
-  label: 'Location',
-  required: true,
-}
-
-// Query by distance
-const nearbyLocations = await payload.find({
-  collection: 'stores',
-  where: {
-    location: {
-      near: [10, 20], // [longitude, latitude]
-      maxDistance: 5000, // in meters
-      minDistance: 1000,
-    },
-  },
-})
-```
-
-### Join Fields (Reverse Relationships)
-
-```typescript
-// From Users collection - show user's orders
-{
-  name: 'orders',
-  type: 'join',
-  collection: 'orders',
-  on: 'customer', // The field in 'orders' that references this user
-}
-```
-
-### Tabs & Groups
-
-```typescript
-// Tabs
-{
-  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)
-{
-  name: 'meta',
-  type: 'group',
-  fields: [
-    { name: 'title', type: 'text' },
-    { name: 'description', type: 'textarea' },
-  ],
-}
-```
-
-## Validation
-
-```typescript
-{
-  name: 'email',
-  type: 'email',
-  validate: (value, { operation, data, siblingData }) => {
-    if (operation === 'create' && !value) {
-      return 'Email is required'
-    }
-    if (value && !value.includes('@')) {
-      return 'Invalid email format'
-    }
-    return true
-  },
-}
-```

+ 0 - 175
.cursor/rules/hooks.md

@@ -1,175 +0,0 @@
----
-title: Hooks
-description: Collection hooks, field hooks, and context patterns
-tags: [payload, hooks, lifecycle, context]
----
-
-# Payload CMS Hooks
-
-## Collection Hooks
-
-```typescript
-export const Posts: CollectionConfig = {
-  slug: 'posts',
-  hooks: {
-    // Before validation - format data
-    beforeValidate: [
-      async ({ data, operation }) => {
-        if (operation === 'create') {
-          data.slug = slugify(data.title)
-        }
-        return data
-      },
-    ],
-
-    // Before save - business logic
-    beforeChange: [
-      async ({ data, req, operation, originalDoc }) => {
-        if (operation === 'update' && data.status === 'published') {
-          data.publishedAt = new Date()
-        }
-        return data
-      },
-    ],
-
-    // After save - side effects
-    afterChange: [
-      async ({ doc, req, operation, previousDoc, context }) => {
-        // Check context to prevent loops
-        if (context.skipNotification) return
-
-        if (operation === 'create') {
-          await sendNotification(doc)
-        }
-        return doc
-      },
-    ],
-
-    // After read - computed fields
-    afterRead: [
-      async ({ doc, req }) => {
-        doc.viewCount = await getViewCount(doc.id)
-        return doc
-      },
-    ],
-
-    // Before delete - cascading deletes
-    beforeDelete: [
-      async ({ req, id }) => {
-        await req.payload.delete({
-          collection: 'comments',
-          where: { post: { equals: id } },
-          req, // Important for transaction
-        })
-      },
-    ],
-  },
-}
-```
-
-## Field Hooks
-
-```typescript
-import type { 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
-}
-
-{
-  name: 'email',
-  type: 'email',
-  hooks: {
-    beforeValidate: [beforeValidateHook],
-    afterRead: [afterReadHook],
-  },
-}
-```
-
-## Hook Context
-
-Share data between hooks or control hook behavior using request context:
-
-```typescript
-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)
-      },
-    ],
-  },
-}
-```
-
-## Next.js Revalidation Pattern
-
-```typescript
-import type { CollectionAfterChangeHook } from 'payload'
-import { revalidatePath } from 'next/cache'
-
-export const revalidatePage: CollectionAfterChangeHook = ({
-  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}`
-      revalidatePath(oldPath)
-    }
-  }
-  return doc
-}
-```
-
-## Date Field Auto-Set
-
-```typescript
-{
-  name: 'publishedOn',
-  type: 'date',
-  hooks: {
-    beforeChange: [
-      ({ siblingData, value }) => {
-        if (siblingData._status === 'published' && !value) {
-          return new Date()
-        }
-        return value
-      },
-    ],
-  },
-}
-```
-
-## 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
-- Use context flags to prevent infinite loops

+ 0 - 126
.cursor/rules/payload-overview.md

@@ -1,126 +0,0 @@
----
-title: Payload CMS Overview
-description: Core principles and quick reference for Payload CMS development
-tags: [payload, overview, quickstart]
----
-
-# Payload CMS Development Rules
-
-You are an expert Payload CMS developer. When working with Payload projects, follow these rules:
-
-## Core Principles
-
-1. **TypeScript-First**: Always use TypeScript with proper types from Payload
-2. **Security-Critical**: Follow all security patterns, especially access control
-3. **Type Generation**: Run `generate:types` script after schema changes
-4. **Transaction Safety**: Always pass `req` to nested operations in hooks
-5. **Access Control**: Understand Local API bypasses access control by default
-
-## Project Structure
-
-```
-src/
-├── app/
-│   ├── (frontend)/          # Frontend routes
-│   └── (payload)/           # Payload admin routes
-├── collections/             # Collection configs
-├── globals/                 # Global configs
-├── components/              # Custom React components
-├── hooks/                   # Hook functions
-├── access/                  # Access control functions
-└── payload.config.ts        # Main config
-```
-
-## Minimal Config Pattern
-
-```typescript
-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,
-  }),
-})
-```
-
-## Getting Payload Instance
-
-```typescript
-// 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>
-}
-```
-
-## Quick Reference
-
-| Task                  | Solution                           |
-| --------------------- | ---------------------------------- |
-| Auto-generate slugs   | `slugField()`                      |
-| Restrict by user      | Access control with query          |
-| Local API user ops    | `user` + `overrideAccess: false`   |
-| Draft/publish         | `versions: { drafts: true }`       |
-| Computed fields       | `virtual: true` with afterRead     |
-| Conditional fields    | `admin.condition`                  |
-| Custom validation     | `validate` function                |
-| Filter relationships  | `filterOptions` on field           |
-| Select fields         | `select` parameter                 |
-| Auto-set dates        | beforeChange hook                  |
-| Prevent loops         | `req.context` check                |
-| Cascading deletes     | beforeDelete hook                  |
-| Geospatial queries    | `point` field with `near`/`within` |
-| Reverse relationships | `join` field type                  |
-| Query relationships   | Nested property syntax             |
-| Complex queries       | AND/OR logic                       |
-| Transactions          | Pass `req` to operations           |
-| Background jobs       | Jobs queue with tasks              |
-| Custom routes         | Collection custom endpoints        |
-| Cloud storage         | Storage adapter plugins            |
-| Multi-language        | `localization` + `localized: true` |
-
-## Resources
-
-- Docs: https://payloadcms.com/docs
-- LLM Context: https://payloadcms.com/llms-full.txt
-- GitHub: https://github.com/payloadcms/payload
-- Examples: https://github.com/payloadcms/payload/tree/main/examples
-- Templates: https://github.com/payloadcms/payload/tree/main/templates

+ 0 - 323
.cursor/rules/plugin-development.md

@@ -1,323 +0,0 @@
----
-title: Plugin Development
-description: Creating Payload CMS plugins with TypeScript patterns
-tags: [payload, plugins, architecture, patterns]
----
-
-# Payload Plugin Development
-
-## Plugin Architecture
-
-Plugins are functions that receive configuration options and return a function that transforms the Payload config:
-
-```typescript
-import type { Config, Plugin } from 'payload'
-
-interface MyPluginConfig {
-  enabled?: boolean
-  collections?: string[]
-}
-
-export const myPlugin =
-  (options: MyPluginConfig): Plugin =>
-  (config: Config): Config => ({
-    ...config,
-    // Transform config here
-  })
-```
-
-**Key Pattern:** Double arrow function (currying)
-
-- First function: Accepts plugin options, returns plugin function
-- Second function: Accepts Payload config, returns modified config
-
-## Adding Fields to Collections
-
-```typescript
-export const seoPlugin =
-  (options: { collections?: string[] }): Plugin =>
-  (config: Config): Config => {
-    const seoFields: Field[] = [
-      {
-        name: 'meta',
-        type: 'group',
-        fields: [
-          { name: 'title', type: 'text' },
-          { name: 'description', type: 'textarea' },
-        ],
-      },
-    ]
-
-    return {
-      ...config,
-      collections: config.collections?.map((collection) => {
-        if (options.collections?.includes(collection.slug)) {
-          return {
-            ...collection,
-            fields: [...(collection.fields || []), ...seoFields],
-          }
-        }
-        return collection
-      }),
-    }
-  }
-```
-
-## Adding New Collections
-
-```typescript
-export const redirectsPlugin =
-  (options: { overrides?: Partial<CollectionConfig> }): Plugin =>
-  (config: Config): Config => {
-    const redirectsCollection: CollectionConfig = {
-      slug: 'redirects',
-      access: { read: () => true },
-      fields: [
-        { name: 'from', type: 'text', required: true, unique: true },
-        { name: 'to', type: 'text', required: true },
-      ],
-      ...options.overrides,
-    }
-
-    return {
-      ...config,
-      collections: [...(config.collections || []), redirectsCollection],
-    }
-  }
-```
-
-## Adding Hooks
-
-```typescript
-const resaveChildrenHook: CollectionAfterChangeHook = async ({ doc, req, operation }) => {
-  if (operation === 'update') {
-    const children = await req.payload.find({
-      collection: 'pages',
-      where: { parent: { equals: doc.id } },
-    })
-
-    for (const child of children.docs) {
-      await req.payload.update({
-        collection: 'pages',
-        id: child.id,
-        data: child,
-      })
-    }
-  }
-  return doc
-}
-
-export const nestedDocsPlugin =
-  (options: { collections: string[] }): Plugin =>
-  (config: Config): Config => ({
-    ...config,
-    collections: (config.collections || []).map((collection) => {
-      if (options.collections.includes(collection.slug)) {
-        return {
-          ...collection,
-          hooks: {
-            ...(collection.hooks || {}),
-            afterChange: [resaveChildrenHook, ...(collection.hooks?.afterChange || [])],
-          },
-        }
-      }
-      return collection
-    }),
-  })
-```
-
-## Adding Root-Level Endpoints
-
-```typescript
-export const seoPlugin =
-  (options: { generateTitle?: (doc: any) => string }): Plugin =>
-  (config: Config): Config => {
-    const generateTitleEndpoint: Endpoint = {
-      path: '/plugin-seo/generate-title',
-      method: 'post',
-      handler: async (req) => {
-        const data = await req.json?.()
-        const result = options.generateTitle ? options.generateTitle(data.doc) : ''
-        return Response.json({ result })
-      },
-    }
-
-    return {
-      ...config,
-      endpoints: [...(config.endpoints ?? []), generateTitleEndpoint],
-    }
-  }
-```
-
-## Field Overrides with Defaults
-
-```typescript
-type FieldsOverride = (args: { defaultFields: Field[] }) => Field[]
-
-interface PluginConfig {
-  collections?: string[]
-  fields?: FieldsOverride
-}
-
-export const myPlugin =
-  (options: PluginConfig): Plugin =>
-  (config: Config): Config => {
-    const defaultFields: Field[] = [
-      { name: 'title', type: 'text' },
-      { name: 'description', type: 'textarea' },
-    ]
-
-    const fields =
-      options.fields && typeof options.fields === 'function'
-        ? options.fields({ defaultFields })
-        : defaultFields
-
-    return {
-      ...config,
-      collections: config.collections?.map((collection) => {
-        if (options.collections?.includes(collection.slug)) {
-          return {
-            ...collection,
-            fields: [...(collection.fields || []), ...fields],
-          }
-        }
-        return collection
-      }),
-    }
-  }
-```
-
-## Disable Plugin Pattern
-
-```typescript
-interface PluginConfig {
-  disabled?: boolean
-  collections?: string[]
-}
-
-export const myPlugin =
-  (options: PluginConfig): Plugin =>
-  (config: Config): Config => {
-    // Always add collections/fields for database schema consistency
-    if (!config.collections) {
-      config.collections = []
-    }
-
-    config.collections.push({
-      slug: 'plugin-collection',
-      fields: [{ name: 'title', type: 'text' }],
-    })
-
-    // If disabled, return early but keep schema changes
-    if (options.disabled) {
-      return config
-    }
-
-    // Add endpoints, hooks, components only when enabled
-    config.endpoints = [
-      ...(config.endpoints ?? []),
-      {
-        path: '/my-endpoint',
-        method: 'get',
-        handler: async () => Response.json({ message: 'Hello' }),
-      },
-    ]
-
-    return config
-  }
-```
-
-## Admin Components
-
-```typescript
-export const myPlugin =
-  (options: PluginConfig): Plugin =>
-  (config: Config): Config => {
-    if (!config.admin) config.admin = {}
-    if (!config.admin.components) config.admin.components = {}
-    if (!config.admin.components.beforeDashboard) {
-      config.admin.components.beforeDashboard = []
-    }
-
-    // Add client component
-    config.admin.components.beforeDashboard.push('my-plugin-name/client#BeforeDashboardClient')
-
-    // Add server component (RSC)
-    config.admin.components.beforeDashboard.push('my-plugin-name/rsc#BeforeDashboardServer')
-
-    return config
-  }
-```
-
-## onInit Hook
-
-```typescript
-export const myPlugin =
-  (options: PluginConfig): Plugin =>
-  (config: Config): Config => {
-    const incomingOnInit = config.onInit
-
-    config.onInit = async (payload) => {
-      // IMPORTANT: Call existing onInit first
-      if (incomingOnInit) await incomingOnInit(payload)
-
-      // Plugin initialization
-      payload.logger.info('Plugin initialized')
-
-      // Example: Seed data
-      const { totalDocs } = await payload.count({
-        collection: 'plugin-collection',
-        where: { id: { equals: 'seeded-by-plugin' } },
-      })
-
-      if (totalDocs === 0) {
-        await payload.create({
-          collection: 'plugin-collection',
-          data: { id: 'seeded-by-plugin' },
-        })
-      }
-    }
-
-    return config
-  }
-```
-
-## Best Practices
-
-### Preserve Existing Config
-
-```typescript
-// ✅ Good
-collections: [...(config.collections || []), newCollection]
-
-// ❌ Bad
-collections: [newCollection]
-```
-
-### Respect User Overrides
-
-```typescript
-const collection: CollectionConfig = {
-  slug: 'redirects',
-  fields: defaultFields,
-  ...options.overrides, // User overrides last
-}
-```
-
-### Hook Composition
-
-```typescript
-hooks: {
-  ...collection.hooks,
-  afterChange: [
-    myHook,
-    ...(collection.hooks?.afterChange || []),
-  ],
-}
-```
-
-### Type Safety
-
-```typescript
-import type { Config, Plugin, CollectionConfig, Field } from 'payload'
-```

+ 0 - 223
.cursor/rules/queries.md

@@ -1,223 +0,0 @@
----
-title: Queries
-description: Local API, REST, and GraphQL query patterns
-tags: [payload, queries, local-api, rest, graphql]
----
-
-# Payload CMS Queries
-
-## Query Operators
-
-```typescript
-// Equals
-{ color: { equals: 'blue' } }
-
-// Not equals
-{ status: { not_equals: 'draft' } }
-
-// Greater/less than
-{ price: { greater_than: 100 } }
-{ age: { less_than_equal: 65 } }
-
-// Contains (case-insensitive)
-{ title: { contains: 'payload' } }
-
-// Like (all words present)
-{ description: { like: 'cms headless' } }
-
-// In/not in
-{ category: { in: ['tech', 'news'] } }
-
-// Exists
-{ image: { exists: true } }
-
-// Near (point fields)
-{ location: { near: [10, 20, 5000] } } // [lng, lat, maxDistance]
-```
-
-## AND/OR Logic
-
-```typescript
-{
-  or: [
-    { color: { equals: 'mint' } },
-    {
-      and: [
-        { color: { equals: 'white' } },
-        { featured: { equals: false } },
-      ],
-    },
-  ],
-}
-```
-
-## Nested Properties
-
-```typescript
-{
-  'author.role': { equals: 'editor' },
-  'meta.featured': { exists: true },
-}
-```
-
-## Local API
-
-```typescript
-// Find documents
-const posts = await payload.find({
-  collection: 'posts',
-  where: {
-    status: { equals: 'published' },
-    'author.name': { contains: 'john' },
-  },
-  depth: 2, // Populate relationships
-  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' },
-  },
-})
-```
-
-## Access Control in Local API
-
-**CRITICAL**: Local API bypasses access control by default (`overrideAccess: true`).
-
-```typescript
-// ❌ WRONG: User is passed but access control is bypassed
-const posts = await payload.find({
-  collection: 'posts',
-  user: currentUser,
-  // Result: Operation runs with ADMIN privileges
-})
-
-// ✅ CORRECT: Respects user's access control permissions
-const posts = await payload.find({
-  collection: 'posts',
-  user: currentUser,
-  overrideAccess: false, // Required to enforce access control
-})
-
-// Administrative operation (intentionally bypass access control)
-const allPosts = await payload.find({
-  collection: 'posts',
-  // No user parameter, overrideAccess defaults to true
-})
-```
-
-**When to use `overrideAccess: false`:**
-
-- Performing operations on behalf of a user
-- Testing access control logic
-- API routes that should respect user permissions
-
-## REST API
-
-```typescript
-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
-
-```
-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
-  }
-}
-```
-
-## 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`

+ 0 - 122
.cursor/rules/security-critical.mdc

@@ -1,122 +0,0 @@
----
-title: Critical Security Patterns
-description: The three most important security patterns in Payload CMS
-tags: [payload, security, critical, access-control, transactions, hooks]
-priority: high
----
-
-# CRITICAL SECURITY PATTERNS
-
-These are the three most critical security patterns that MUST be followed in every Payload CMS project.
-
-## 1. Local API Access Control (MOST IMPORTANT)
-
-**By default, Local API operations bypass ALL access control**, even when passing a user.
-
-```typescript
-// ❌ 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
-})
-
-// ✅ Administrative operation (intentional bypass)
-await payload.find({
-  collection: 'posts',
-  // No user, overrideAccess defaults to true
-})
-```
-
-**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)
-
-**Rule**: When passing `user` to Local API, ALWAYS set `overrideAccess: false`
-
-## 2. Transaction Safety in Hooks
-
-**Nested operations in hooks without `req` break transaction atomicity.**
-
-```typescript
-// ❌ 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
-      })
-    },
-  ]
-}
-```
-
-**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
-
-**Rule**: ALWAYS pass `req` to nested operations in hooks
-
-## 3. Prevent Infinite Hook Loops
-
-**Hooks triggering operations that trigger the same hooks create infinite loops.**
-
-```typescript
-// ❌ 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,
-      })
-    },
-  ]
-}
-```
-
-**Rule**: Use `req.context` flags to prevent hook loops

+ 4 - 2
.env.example

@@ -1,2 +1,4 @@
-DATABASE_URL=mongodb://127.0.0.1/your-database-name
-PAYLOAD_SECRET=YOUR_SECRET_HERE
+DATABASE_URL=postgresql://neondb_owner:npg_MvO9GDngkNz6@ep-polished-bush-a1c3hb07-pooler.ap-southeast-1.aws.neon.tech/neondb?sslmode=require
+PAYLOAD_SECRET=1b33e1474a2980dddaceb6eb
+NODE_ENV='test'
+BLOB_READ_WRITE_TOKEN=vercel_blob_rw_U0cdGclRijn1yww0_wTdZWerUbbeEQyRErHfMKPyBTxEY9a

+ 184 - 0
GALLERY_API_DOCUMENTATION.md

@@ -0,0 +1,184 @@
+# Gallery API Documentation
+
+Dokumentasi API untuk collection `gallery` yang berisi foto-foto dengan caption.
+
+## Base Endpoint
+
+```
+/api/gallery
+```
+
+## Authentication
+
+Endpoint ini bersifat **public read-only**, tidak memerlukan authentication untuk membaca data.
+
+---
+
+## Endpoints
+
+### GET `/api/gallery`
+Mengambil daftar gallery items dengan pagination dan filtering.
+
+**Query Parameters:**
+- `page` - Nomor halaman (default: 1)
+- `limit` - Jumlah item per halaman (default: 10)
+- `sort` - Sorting field (contoh: `-createdAt` untuk newest first, `createdAt` untuk oldest first)
+- `depth` - Depth untuk populate image relationship (default: 0, gunakan 1 untuk populate image object dengan URL)
+
+**Response Structure:**
+```json
+{
+  "docs": [
+    {
+      "id": 1,
+      "image": 123, // ID media (jika depth=0) atau object Media (jika depth=1)
+      "caption": "Event foto description",
+      "createdAt": "2025-01-15T10:00:00.000Z",
+      "updatedAt": "2025-01-15T10:00:00.000Z"
+    }
+  ],
+  "totalDocs": 50,
+  "limit": 10,
+  "totalPages": 5,
+  "page": 1,
+  "hasPrevPage": false,
+  "hasNextPage": true
+}
+```
+
+**Gallery Object Fields:**
+- `id` - Unique identifier (number)
+- `image` - Media ID (number) atau Media object (jika depth=1)
+- `caption` - Caption/deskripsi foto (string, required)
+- `createdAt` - Creation timestamp (ISO date string)
+- `updatedAt` - Last update timestamp (ISO date string)
+
+### GET `/api/gallery/:id`
+Mengambil single gallery item berdasarkan ID.
+
+**Path Parameters:**
+- `id` - Gallery item ID
+
+**Query Parameters:**
+- `depth` - Depth untuk populate image relationship (recommended: 1 untuk mendapatkan URL gambar)
+
+**Response:** Single gallery object
+
+---
+
+## Cara Menggunakan di Frontend
+
+### 1. Fetch List Gallery Items
+
+```javascript
+// Mengambil 12 gambar terbaru
+const response = await fetch('http://localhost:3001/api/gallery?limit=12&sort=-createdAt&depth=1')
+const data = await response.json()
+
+// data.docs berisi array gallery items
+data.docs.forEach(item => {
+  console.log(item.caption)
+  console.log(item.image.url) // URL gambar jika depth=1
+})
+```
+
+### 2. Fetch Single Gallery Item
+
+```javascript
+const response = await fetch('http://localhost:3001/api/gallery/123?depth=1')
+const item = await response.json()
+
+console.log(item.caption)
+console.log(item.image.url) // URL gambar
+```
+
+### 3. Pagination
+
+```javascript
+// Halaman 2, 20 item per halaman
+const response = await fetch('http://localhost:3001/api/gallery?page=2&limit=20&depth=1')
+const data = await response.json()
+
+// Check apakah ada halaman selanjutnya
+if (data.hasNextPage) {
+  // Load more...
+}
+```
+
+### 4. Display Gallery Grid
+
+**React Component Example:**
+```jsx
+function GalleryGrid() {
+  const [items, setItems] = useState([])
+  const [loading, setLoading] = useState(true)
+
+  useEffect(() => {
+    fetch('http://localhost:3001/api/gallery?limit=20&sort=-createdAt&depth=1')
+      .then(res => res.json())
+      .then(data => {
+        setItems(data.docs)
+        setLoading(false)
+      })
+  }, [])
+
+  if (loading) return <div>Loading...</div>
+
+  return (
+    <div className="gallery-grid">
+      {items.map(item => (
+        <div key={item.id} className="gallery-item">
+          <img 
+            src={item.image.url} 
+            alt={item.image.alt || item.caption}
+            loading="lazy"
+          />
+          <p className="caption">{item.caption}</p>
+        </div>
+      ))}
+    </div>
+  )
+}
+```
+
+---
+
+## Important Notes
+
+1. **Gunakan `depth=1`** untuk mendapatkan object `image` lengkap (dengan URL, width, height, dll) tanpa perlu fetch terpisah
+2. **Default sorting**: Gunakan `sort=-createdAt` untuk menampilkan gambar terbaru terlebih dahulu
+3. **Image URL**: Ketika `depth=1`, `item.image.url` berisi URL lengkap untuk menampilkan gambar
+4. **Alt text**: Gunakan `item.image.alt` untuk accessibility atau fallback ke `item.caption`
+5. **Pagination**: Gunakan `hasNextPage` dan `hasPrevPage` untuk implementasi infinite scroll atau pagination UI
+
+---
+
+## Media Object (ketika depth=1)
+
+Struktur lengkap object `image` ketika menggunakan `depth=1`:
+
+```json
+{
+  "id": 123,
+  "alt": "Image description",
+  "filename": "photo.jpg",
+  "mimeType": "image/jpeg",
+  "filesize": 123456,
+  "width": 1920,
+  "height": 1080,
+  "url": "http://localhost:3001/media/photo.jpg",
+  "createdAt": "2025-01-15T10:00:00.000Z",
+  "updatedAt": "2025-01-15T10:00:00.000Z"
+}
+```
+
+---
+
+## Best Practices
+
+1. **Lazy Loading**: Gunakan `loading="lazy"` pada tag `<img>` untuk performa yang lebih baik
+2. **Responsive Images**: Gunakan `width` dan `height` dari media object untuk implementasi responsive images
+3. **Error Handling**: Selalu handle error saat fetch dan pastikan `image` object tidak null
+4. **Pagination**: Implement pagination atau infinite scroll untuk gallery yang besar
+5. **Image Optimization**: Pertimbangkan menggunakan image CDN atau Next.js Image component untuk optimasi
+

+ 4 - 2
next.config.mjs

@@ -3,8 +3,10 @@ import { withPayload } from '@payloadcms/next/withPayload'
 /** @type {import('next').NextConfig} */
 const nextConfig = {
   // Your Next.js config here
-  serverActions: {
-    bodySizeLimit: '50mb', // Increase limit for image uploads
+  experimental: {
+    serverActions: {
+      bodySizeLimit: '50mb', // Increase limit for image uploads
+    },
   },
   webpack: (webpackConfig) => {
     webpackConfig.resolve.extensionAlias = {

+ 3 - 0
src/collections/Authors.ts

@@ -6,6 +6,9 @@ export const Authors: CollectionConfig = {
     useAsTitle: 'name',
     defaultColumns: ['name', 'socialMediaLink'],
   },
+  access: {
+    read: () => true,
+  },
   fields: [
     {
       name: 'name',

+ 29 - 0
src/collections/Gallery.ts

@@ -0,0 +1,29 @@
+import type { CollectionConfig } from 'payload'
+
+export const Gallery: CollectionConfig = {
+  slug: 'gallery',
+  admin: {
+    useAsTitle: 'caption',
+    defaultColumns: ['caption', 'image', 'createdAt'],
+  },
+  access: {
+    read: () => true,
+  },
+  fields: [
+    {
+      name: 'image',
+      label: 'Photo',
+      type: 'upload',
+      relationTo: 'media',
+      required: true,
+    },
+    {
+      name: 'caption',
+      label: 'Caption',
+      type: 'text',
+      required: true,
+    },
+  ],
+  timestamps: true,
+}
+

+ 1 - 2
src/collections/Users.ts

@@ -7,7 +7,6 @@ export const Users: CollectionConfig = {
   },
   auth: true,
   fields: [
-    // Email added by default
-    // Add more fields as needed
+
   ],
 }

+ 27 - 0
src/payload-types.ts

@@ -73,6 +73,7 @@ export interface Config {
     posts: Post;
     clients: Client;
     careers: Career;
+    gallery: Gallery;
     'payload-kv': PayloadKv;
     'payload-locked-documents': PayloadLockedDocument;
     'payload-preferences': PayloadPreference;
@@ -86,6 +87,7 @@ export interface Config {
     posts: PostsSelect<false> | PostsSelect<true>;
     clients: ClientsSelect<false> | ClientsSelect<true>;
     careers: CareersSelect<false> | CareersSelect<true>;
+    gallery: GallerySelect<false> | GallerySelect<true>;
     'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
     'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
     'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
@@ -266,6 +268,17 @@ export interface Career {
   updatedAt: string;
   createdAt: string;
 }
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "gallery".
+ */
+export interface Gallery {
+  id: number;
+  image: number | Media;
+  caption: string;
+  updatedAt: string;
+  createdAt: string;
+}
 /**
  * This interface was referenced by `Config`'s JSON-Schema
  * via the `definition` "payload-kv".
@@ -313,6 +326,10 @@ export interface PayloadLockedDocument {
     | ({
         relationTo: 'careers';
         value: number | Career;
+      } | null)
+    | ({
+        relationTo: 'gallery';
+        value: number | Gallery;
       } | null);
   globalSlug?: string | null;
   user: {
@@ -465,6 +482,16 @@ export interface CareersSelect<T extends boolean = true> {
   updatedAt?: T;
   createdAt?: T;
 }
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "gallery_select".
+ */
+export interface GallerySelect<T extends boolean = true> {
+  image?: T;
+  caption?: T;
+  updatedAt?: T;
+  createdAt?: T;
+}
 /**
  * This interface was referenced by `Config`'s JSON-Schema
  * via the `definition` "payload-kv_select".

+ 4 - 3
src/payload.config.ts

@@ -12,6 +12,7 @@ import { Authors } from './collections/Authors'
 import { Posts } from './collections/Posts'
 import { Clients } from './collections/Clients'
 import { Careers } from './collections/Careers'
+import { Gallery } from './collections/Gallery'
 
 const filename = fileURLToPath(import.meta.url)
 const dirname = path.dirname(filename)
@@ -23,7 +24,7 @@ export default buildConfig({
       baseDir: path.resolve(dirname),
     },
   },
-  collections: [Users, Media, Authors, Posts, Clients, Careers],
+  collections: [Users, Media, Authors, Posts, Clients, Careers, Gallery],
   editor: lexicalEditor(),
   secret: process.env.PAYLOAD_SECRET || '',
   typescript: {
@@ -38,11 +39,11 @@ export default buildConfig({
   cors: '*',
   plugins: [
     vercelBlobStorage({
-      enabled: true,
+      enabled: process.env.NODE_ENV === 'test',
       collections: {
         media: true, // Aktifkan untuk collection 'media'
       },
-      token: process.env.BLOB_READ_WRITE_TOKEN, // Nanti didapat dari dashboard Vercel
+      token: process.env.BLOB_READ_WRITE_TOKEN || '', // Nanti didapat dari dashboard Vercel
     }),
   ],
 })