init commit for iPAL-kit plugin
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import type { Field } from 'payload'
|
||||
|
||||
import { CONSENT_CATEGORIES } from '../../modules/consent/index.js'
|
||||
|
||||
/**
|
||||
* Fields for the CookieSettings global — GDPR consent banner copy, editable
|
||||
* per locale. The privacy-policy link is NOT stored here: it comes from the
|
||||
* pages module (system page role 'privacyPolicy'), keeping one source of truth.
|
||||
*/
|
||||
export const cookieSettingsFields: Field[] = [
|
||||
{
|
||||
name: 'message',
|
||||
type: 'textarea',
|
||||
admin: {
|
||||
description: 'Main consent message shown in the banner.',
|
||||
},
|
||||
localized: true,
|
||||
},
|
||||
{
|
||||
name: 'settingsTitle',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: 'Heading for the detailed settings panel.',
|
||||
},
|
||||
localized: true,
|
||||
},
|
||||
{
|
||||
name: 'buttons',
|
||||
type: 'group',
|
||||
admin: {
|
||||
description: 'Button labels.',
|
||||
},
|
||||
fields: [
|
||||
{ name: 'acceptAll', type: 'text', localized: true },
|
||||
{ name: 'reject', type: 'text', localized: true },
|
||||
{ name: 'settings', type: 'text', localized: true },
|
||||
{ name: 'save', type: 'text', localized: true },
|
||||
{ name: 'back', type: 'text', localized: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'categories',
|
||||
type: 'array',
|
||||
admin: {
|
||||
description: 'Per-category titles and descriptions.',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'key',
|
||||
type: 'select',
|
||||
options: CONSENT_CATEGORIES.map((c) => ({ label: c, value: c })),
|
||||
required: true,
|
||||
},
|
||||
{ name: 'title', type: 'text', localized: true },
|
||||
{ name: 'description', type: 'textarea', localized: true },
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { GlobalConfig } from 'payload'
|
||||
|
||||
import { cookieSettingsFields } from './fields.js'
|
||||
|
||||
/**
|
||||
* Builds the CookieSettings global — consent banner copy managed by editors.
|
||||
* Public read (the banner needs it on the frontend for anonymous visitors).
|
||||
*/
|
||||
export function buildCookieSettings(): GlobalConfig {
|
||||
return {
|
||||
slug: 'cookie-settings',
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
admin: {
|
||||
group: 'Settings',
|
||||
},
|
||||
fields: cookieSettingsFields,
|
||||
label: 'Cookie Settings',
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { Field } from 'payload'
|
||||
|
||||
/**
|
||||
* Analytics configuration — GA4 measurement ID and GTM container ID.
|
||||
* IDs are public (exposed client-side), so no read restriction needed.
|
||||
*/
|
||||
export const analyticsFields: Field[] = [
|
||||
{
|
||||
name: 'ga4MeasurementId',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: 'Google Analytics 4 Measurement ID.',
|
||||
placeholder: 'G-XXXXXXXXXX',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'gtmContainerId',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: 'Google Tag Manager container ID.',
|
||||
placeholder: 'GTM-XXXXXXX',
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { Field } from 'payload'
|
||||
|
||||
/**
|
||||
* SMTP transport settings for outbound email.
|
||||
*
|
||||
* Protected at the global level (SiteIntegrations requires an authenticated
|
||||
* user), so all fields — including the password — stay editable in the admin
|
||||
* panel while remaining inaccessible to anonymous API requests.
|
||||
*/
|
||||
export const smtpFields: Field[] = [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'smtpHost',
|
||||
type: 'text',
|
||||
admin: { placeholder: 'smtp.example.com', width: '70%' },
|
||||
},
|
||||
{
|
||||
name: 'smtpPort',
|
||||
type: 'number',
|
||||
admin: { width: '30%' },
|
||||
defaultValue: 587,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'smtpUser',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: 'SMTP account username.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'smtpPassword',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: 'SMTP account password.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'smtpFromAddress',
|
||||
type: 'email',
|
||||
admin: {
|
||||
description: 'Default "from" address for outgoing mail.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'smtpFromName',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: 'Default "from" display name.',
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { Field } from 'payload'
|
||||
|
||||
/**
|
||||
* Cloudflare R2 storage credentials.
|
||||
* Reserved for future use — media offloading to R2.
|
||||
*
|
||||
* Protected at the global level (SiteIntegrations requires an authenticated
|
||||
* user), so the access keys stay editable in the admin panel while remaining
|
||||
* inaccessible to anonymous API requests.
|
||||
*/
|
||||
export const storageFields: Field[] = [
|
||||
{
|
||||
name: 'r2Bucket',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: 'R2 bucket name.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'r2Endpoint',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: 'R2 S3-compatible endpoint URL.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'r2AccessKeyId',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: 'R2 access key ID.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'r2SecretAccessKey',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: 'R2 secret access key.',
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { Field } from 'payload'
|
||||
|
||||
/**
|
||||
* Cloudflare Turnstile credentials.
|
||||
*
|
||||
* siteKey is public (rendered in the widget); secretKey is used for
|
||||
* server-side verification. Both are protected at the global level
|
||||
* (SiteIntegrations requires an authenticated user) rather than per-field,
|
||||
* so they remain editable in the admin panel.
|
||||
*/
|
||||
export const turnstileFields: Field[] = [
|
||||
{
|
||||
name: 'turnstileSiteKey',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: 'Public site key rendered in the Turnstile widget.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'turnstileSecretKey',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: 'Secret key used for server-side verification.',
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { Field, GlobalConfig } from 'payload'
|
||||
|
||||
import { isAdmin } from '../../modules/access/index.js'
|
||||
import { analyticsFields } from './fields/analytics.js'
|
||||
import { smtpFields } from './fields/smtp.js'
|
||||
import { storageFields } from './fields/storage.js'
|
||||
import { turnstileFields } from './fields/turnstile.js'
|
||||
|
||||
type BuildSiteIntegrationsArgs = {
|
||||
/** Extra fields injected by the client project */
|
||||
additionalFields?: Field[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the SiteIntegrations global.
|
||||
*
|
||||
* Holds third-party service credentials. Access is enforced at the global
|
||||
* level — the whole global requires an authenticated user — so secrets stay
|
||||
* out of anonymous API responses while remaining editable in the admin panel
|
||||
* and readable via the server-side Local API. (Field-level read:false was
|
||||
* avoided because it also hides fields from the admin UI, making them
|
||||
* impossible to enter.)
|
||||
*
|
||||
* Unnamed tabs keep data flat (siteIntegrations.ga4MeasurementId).
|
||||
*/
|
||||
export function buildSiteIntegrations({
|
||||
additionalFields,
|
||||
}: BuildSiteIntegrationsArgs = {}): GlobalConfig {
|
||||
return {
|
||||
slug: 'site-integrations',
|
||||
access: {
|
||||
// Admin-only — secrets live here. Anonymous and non-admin users get
|
||||
// nothing through the API; admins read/edit in the panel and via Local API.
|
||||
read: ({ req: { user } }) => isAdmin(user),
|
||||
update: ({ req: { user } }) => isAdmin(user),
|
||||
},
|
||||
admin: {
|
||||
group: 'Settings',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{ fields: analyticsFields, label: 'Analytics' },
|
||||
{ fields: turnstileFields, label: 'Turnstile' },
|
||||
{ fields: smtpFields, label: 'SMTP' },
|
||||
{ fields: storageFields, label: 'Storage' },
|
||||
...(additionalFields?.length ? [{ fields: additionalFields, label: 'Custom' }] : []),
|
||||
],
|
||||
},
|
||||
],
|
||||
label: 'Site Integrations',
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { Field } from 'payload'
|
||||
|
||||
/**
|
||||
* General site identity fields.
|
||||
* Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo).
|
||||
*/
|
||||
export const generalFields: Field[] = [
|
||||
{
|
||||
name: 'siteName',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: 'Used in page titles and Open Graph metadata.',
|
||||
},
|
||||
localized: true,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'titleOrder',
|
||||
type: 'select',
|
||||
admin: {
|
||||
description: 'Which comes first in browser tabs.',
|
||||
},
|
||||
defaultValue: 'page-first',
|
||||
options: [
|
||||
{ label: 'Page first — About Us | Acme', value: 'page-first' },
|
||||
{ label: 'Site first — Acme | About Us', value: 'site-first' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'titleSeparator',
|
||||
type: 'select',
|
||||
admin: {
|
||||
description: 'Separates the page title from the site name in browser tabs.',
|
||||
},
|
||||
defaultValue: '|',
|
||||
options: [
|
||||
{ label: 'Pipe — Page | Site', value: '|' },
|
||||
{ label: 'Dash — Page – Site', value: '–' },
|
||||
{ label: 'Hyphen — Page - Site', value: '-' },
|
||||
{ label: 'Bullet — Page · Site', value: '·' },
|
||||
{ label: 'Slash — Page / Site', value: '/' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'logo',
|
||||
type: 'upload',
|
||||
admin: {
|
||||
description: 'Primary site logo.',
|
||||
},
|
||||
relationTo: 'media',
|
||||
},
|
||||
{
|
||||
name: 'defaultShareImage',
|
||||
type: 'upload',
|
||||
admin: {
|
||||
description: 'Fallback Open Graph image when a page has none.',
|
||||
},
|
||||
relationTo: 'media',
|
||||
},
|
||||
{
|
||||
name: 'favicon',
|
||||
type: 'upload',
|
||||
admin: {
|
||||
description: 'Square source icon (PNG or SVG) for the browser tab. Rendered by the frontend.',
|
||||
},
|
||||
relationTo: 'media',
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { Field } from 'payload'
|
||||
|
||||
/**
|
||||
* Theme behavior configuration.
|
||||
*
|
||||
* These are decisions, not visuals — the plugin stores them, the client
|
||||
* template reads them and decides whether to render a toggle and which
|
||||
* mode to start in. Appearance itself stays in the template.
|
||||
*/
|
||||
export const themeFields: Field[] = [
|
||||
{
|
||||
name: 'defaultTheme',
|
||||
type: 'select',
|
||||
admin: {
|
||||
description: 'Theme applied on a visitor’s first visit.',
|
||||
},
|
||||
defaultValue: 'light',
|
||||
options: [
|
||||
{ label: 'Light', value: 'light' },
|
||||
{ label: 'Dark', value: 'dark' },
|
||||
],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'allowThemeToggle',
|
||||
type: 'checkbox',
|
||||
admin: {
|
||||
description: 'Show a light/dark switch on the site.',
|
||||
},
|
||||
defaultValue: true,
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { Field, GlobalConfig } from 'payload'
|
||||
|
||||
import type { ContentOption } from '../../modules/content/index.js'
|
||||
import type { PagesOption } from '../../modules/pages/index.js'
|
||||
|
||||
import { buildArchiveFields } from '../../modules/content/index.js'
|
||||
import { buildSystemPagesFields } from '../../modules/pages/index.js'
|
||||
import { generalFields } from './fields/general.js'
|
||||
import { themeFields } from './fields/theme.js'
|
||||
|
||||
type BuildSiteSettingsArgs = {
|
||||
/** Extra fields injected by the client project */
|
||||
additionalFields?: Field[]
|
||||
/** Archive-page assignments for content collections — joins the same tab */
|
||||
content?: ContentOption
|
||||
/** System-page assignments — adds a "System Pages" tab when provided */
|
||||
pages?: PagesOption
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the SiteSettings global.
|
||||
*
|
||||
* Uses unnamed tabs — data stays flat (siteSettings.siteName, not
|
||||
* siteSettings.general.siteName). Client-provided fields land in their
|
||||
* own "Custom" tab so core data paths never shift.
|
||||
*/
|
||||
export function buildSiteSettings({
|
||||
additionalFields,
|
||||
content,
|
||||
pages,
|
||||
}: BuildSiteSettingsArgs = {}): GlobalConfig {
|
||||
// Archive assignments sit with the system pages: both answer "which page
|
||||
// plays this role", and both turn into URLs through the page's own slug.
|
||||
const systemPageFields = [
|
||||
...(pages ? buildSystemPagesFields(pages) : []),
|
||||
...(content && pages ? buildArchiveFields(content, pages.slug) : []),
|
||||
]
|
||||
return {
|
||||
slug: 'site-settings',
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
admin: {
|
||||
group: 'Settings',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
fields: generalFields,
|
||||
label: 'General',
|
||||
},
|
||||
{
|
||||
fields: themeFields,
|
||||
label: 'Theme',
|
||||
},
|
||||
...(systemPageFields.length ? [{ fields: systemPageFields, label: 'System Pages' }] : []),
|
||||
...(additionalFields?.length ? [{ fields: additionalFields, label: 'Custom' }] : []),
|
||||
],
|
||||
},
|
||||
],
|
||||
label: 'Site Settings',
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user