feat: add Services collection and block

- Add ServiceCategories collection for grouping services
- Add Services collection with comprehensive service profiles:
  - Title, slug, descriptions (short + full)
  - Icon (text or image) and image gallery
  - Category relationship for grouping
  - Features/benefits array
  - Flexible pricing (on-request default, fixed, hourly, range, etc.)
  - CTA buttons (primary + secondary)
  - Related services, team members, and FAQs relationships
  - Detail page sections with testimonials
  - SEO fields (meta title, description, OG image)
  - Status flags (active, featured, new badge)
- Add ServicesBlock with 8 layouts:
  - Grid, List, Tabs, Accordion, Featured+Grid, Slider, Compact, Masonry
- Multi-tenant enabled via plugin configuration
- Update documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Martin Porwoll 2025-12-10 07:39:03 +00:00
parent 261a126f08
commit 8868a5be30
10 changed files with 20412 additions and 1 deletions

View file

@ -50,6 +50,8 @@ Internet → 37.24.237.181 → Caddy (443) → Payload (3000)
│ │ ├── PortfolioCategories.ts
│ │ ├── FAQs.ts
│ │ ├── Team.ts
│ │ ├── ServiceCategories.ts
│ │ ├── Services.ts
│ │ ├── EmailLogs.ts
│ │ ├── AuditLogs.ts
│ │ └── ...
@ -404,6 +406,8 @@ SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT 10;
| Testimonials | testimonials | Kundenbewertungen |
| FAQs | faqs | Häufig gestellte Fragen (FAQ) |
| Team | team | Team-Mitglieder und Mitarbeiter |
| ServiceCategories | service-categories | Kategorien für Leistungen |
| Services | services | Leistungen und Dienstleistungen |
| NewsletterSubscribers | newsletter-subscribers | Newsletter mit Double Opt-In |
| SocialLinks | social-links | Social Media Links |
| Forms | forms | Formular-Builder |

391
src/blocks/ServicesBlock.ts Normal file
View file

@ -0,0 +1,391 @@
import type { Block } from 'payload'
/**
* Services Block
*
* Zeigt Leistungen/Dienstleistungen aus der Services Collection.
* Unterstützt verschiedene Layouts und Filteroptionen.
*/
export const ServicesBlock: Block = {
slug: 'services-block',
labels: {
singular: 'Leistungen',
plural: 'Leistungen',
},
fields: [
{
name: 'title',
type: 'text',
defaultValue: 'Unsere Leistungen',
label: 'Überschrift',
localized: true,
},
{
name: 'subtitle',
type: 'text',
label: 'Untertitel',
localized: true,
},
{
name: 'introduction',
type: 'richText',
label: 'Einleitungstext',
localized: true,
admin: {
description: 'Optionaler Text vor der Leistungsübersicht',
},
},
// Auswahl-Modus
{
name: 'displayMode',
type: 'select',
defaultValue: 'all',
label: 'Auswahl',
options: [
{ label: 'Alle aktiven Leistungen', value: 'all' },
{ label: 'Nur hervorgehobene', value: 'featured' },
{ label: 'Nach Kategorie', value: 'category' },
{ label: 'Handverlesene Auswahl', value: 'selected' },
],
},
{
name: 'category',
type: 'relationship',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
relationTo: 'service-categories' as any,
label: 'Kategorie filtern',
admin: {
condition: (data, siblingData) => siblingData?.displayMode === 'category',
description: 'Zeigt nur Leistungen dieser Kategorie',
},
},
{
name: 'selectedServices',
type: 'relationship',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
relationTo: 'services' as any,
hasMany: true,
label: 'Leistungen auswählen',
admin: {
condition: (data, siblingData) => siblingData?.displayMode === 'selected',
},
},
{
name: 'limit',
type: 'number',
defaultValue: 12,
min: 1,
max: 50,
label: 'Maximale Anzahl',
admin: {
condition: (data, siblingData) =>
siblingData?.displayMode === 'all' ||
siblingData?.displayMode === 'featured' ||
siblingData?.displayMode === 'category',
},
},
// Layout
{
name: 'layout',
type: 'select',
defaultValue: 'grid',
label: 'Layout',
options: [
{ label: 'Grid (Karten)', value: 'grid' },
{ label: 'Liste mit Icons', value: 'list' },
{ label: 'Tabs (nach Kategorie)', value: 'tabs' },
{ label: 'Accordion', value: 'accordion' },
{ label: 'Featured + Grid', value: 'featured-grid' },
{ label: 'Slider/Karussell', value: 'slider' },
{ label: 'Kompakt (nur Titel + Icon)', value: 'compact' },
{ label: 'Masonry', value: 'masonry' },
],
},
{
name: 'columns',
type: 'select',
defaultValue: '3',
label: 'Spalten',
options: [
{ label: '2 Spalten', value: '2' },
{ label: '3 Spalten', value: '3' },
{ label: '4 Spalten', value: '4' },
],
admin: {
condition: (data, siblingData) =>
['grid', 'compact', 'masonry', 'featured-grid'].includes(siblingData?.layout),
},
},
{
name: 'featuredCount',
type: 'number',
defaultValue: 1,
min: 1,
max: 3,
label: 'Anzahl Featured',
admin: {
condition: (data, siblingData) => siblingData?.layout === 'featured-grid',
description: 'Anzahl der hervorgehobenen Leistungen oben',
},
},
// Tabs-Optionen
{
name: 'tabsStyle',
type: 'select',
defaultValue: 'horizontal',
label: 'Tabs-Stil',
options: [
{ label: 'Horizontal', value: 'horizontal' },
{ label: 'Vertikal (Sidebar)', value: 'vertical' },
{ label: 'Pills', value: 'pills' },
],
admin: {
condition: (data, siblingData) => siblingData?.layout === 'tabs',
},
},
{
name: 'showAllTab',
type: 'checkbox',
defaultValue: true,
label: '"Alle" Tab anzeigen',
admin: {
condition: (data, siblingData) => siblingData?.layout === 'tabs',
},
},
// Accordion-Optionen
{
name: 'expandFirst',
type: 'checkbox',
defaultValue: true,
label: 'Erste Leistung automatisch öffnen',
admin: {
condition: (data, siblingData) => siblingData?.layout === 'accordion',
},
},
{
name: 'allowMultipleOpen',
type: 'checkbox',
defaultValue: false,
label: 'Mehrere gleichzeitig öffnen erlauben',
admin: {
condition: (data, siblingData) => siblingData?.layout === 'accordion',
},
},
// Slider-Optionen
{
name: 'autoplay',
type: 'checkbox',
defaultValue: true,
label: 'Automatisch wechseln',
admin: {
condition: (data, siblingData) => siblingData?.layout === 'slider',
},
},
{
name: 'autoplaySpeed',
type: 'number',
defaultValue: 5000,
min: 2000,
max: 15000,
label: 'Wechselintervall (ms)',
admin: {
condition: (data, siblingData) =>
siblingData?.layout === 'slider' && siblingData?.autoplay,
},
},
{
name: 'slidesPerView',
type: 'select',
defaultValue: '3',
label: 'Sichtbare Slides',
options: [
{ label: '1', value: '1' },
{ label: '2', value: '2' },
{ label: '3', value: '3' },
{ label: '4', value: '4' },
],
admin: {
condition: (data, siblingData) => siblingData?.layout === 'slider',
},
},
// Anzeigeoptionen
{
name: 'showImage',
type: 'checkbox',
defaultValue: true,
label: 'Bild anzeigen',
admin: {
condition: (data, siblingData) =>
!['compact', 'list'].includes(siblingData?.layout),
},
},
{
name: 'showIcon',
type: 'checkbox',
defaultValue: true,
label: 'Icon anzeigen',
},
{
name: 'showDescription',
type: 'checkbox',
defaultValue: true,
label: 'Kurzbeschreibung anzeigen',
admin: {
condition: (data, siblingData) => siblingData?.layout !== 'compact',
},
},
{
name: 'showCategory',
type: 'checkbox',
defaultValue: false,
label: 'Kategorie anzeigen',
},
{
name: 'showPricing',
type: 'checkbox',
defaultValue: true,
label: 'Preis anzeigen',
},
{
name: 'showFeatures',
type: 'checkbox',
defaultValue: false,
label: 'Leistungsmerkmale anzeigen',
admin: {
condition: (data, siblingData) =>
['list', 'accordion', 'featured-grid'].includes(siblingData?.layout),
},
},
{
name: 'featuresLimit',
type: 'number',
defaultValue: 3,
min: 1,
max: 10,
label: 'Max. Anzahl Merkmale',
admin: {
condition: (data, siblingData) => siblingData?.showFeatures,
},
},
{
name: 'showCTA',
type: 'checkbox',
defaultValue: true,
label: 'CTA-Button anzeigen',
},
{
name: 'showNewBadge',
type: 'checkbox',
defaultValue: true,
label: '"Neu"-Badge anzeigen',
},
// Gruppierung
{
name: 'groupByCategory',
type: 'checkbox',
defaultValue: false,
label: 'Nach Kategorie gruppieren',
admin: {
condition: (data, siblingData) =>
siblingData?.displayMode !== 'category' &&
!['tabs', 'slider'].includes(siblingData?.layout),
description: 'Zeigt Kategorie-Überschriften zwischen den Leistungen',
},
},
// Detail-Link
{
name: 'linkToDetail',
type: 'checkbox',
defaultValue: true,
label: 'Zur Detail-Seite verlinken',
admin: {
description: 'Karten/Einträge verlinken zur Leistungs-Detail-Seite',
},
},
{
name: 'detailLinkText',
type: 'text',
defaultValue: 'Mehr erfahren',
label: 'Detail-Link Text',
localized: true,
admin: {
condition: (data, siblingData) => siblingData?.linkToDetail,
},
},
{
name: 'servicesBasePath',
type: 'text',
defaultValue: '/leistungen',
label: 'Basis-Pfad für Detail-Seiten',
admin: {
condition: (data, siblingData) => siblingData?.linkToDetail,
description: 'z.B. "/leistungen" → "/leistungen/intensivpflege"',
},
},
// Styling
{
name: 'cardStyle',
type: 'select',
defaultValue: 'elevated',
label: 'Karten-Stil',
options: [
{ label: 'Erhöht (Schatten)', value: 'elevated' },
{ label: 'Umrandet', value: 'bordered' },
{ label: 'Flach', value: 'flat' },
{ label: 'Gefüllt', value: 'filled' },
],
admin: {
condition: (data, siblingData) =>
['grid', 'masonry', 'featured-grid', 'slider'].includes(siblingData?.layout),
},
},
{
name: 'backgroundColor',
type: 'select',
defaultValue: 'white',
label: 'Hintergrund',
options: [
{ label: 'Weiß', value: 'white' },
{ label: 'Hell (Grau)', value: 'light' },
{ label: 'Dunkel', value: 'dark' },
{ label: 'Akzentfarbe', value: 'accent' },
],
},
// Globaler CTA
{
type: 'collapsible',
label: 'Abschnitt-CTA',
admin: {
initCollapsed: true,
description: 'Optionaler Button am Ende des gesamten Blocks',
},
fields: [
{
name: 'showSectionCTA',
type: 'checkbox',
defaultValue: false,
label: 'CTA-Button anzeigen',
},
{
name: 'sectionCTAText',
type: 'text',
defaultValue: 'Alle Leistungen ansehen',
label: 'Button-Text',
localized: true,
admin: {
condition: (data, siblingData) => siblingData?.showSectionCTA,
},
},
{
name: 'sectionCTALink',
type: 'text',
defaultValue: '/leistungen',
label: 'Button-Link',
admin: {
condition: (data, siblingData) => siblingData?.showSectionCTA,
},
},
],
},
],
}

View file

@ -16,3 +16,4 @@ export { NewsletterBlock } from './NewsletterBlock'
export { ProcessStepsBlock } from './ProcessStepsBlock'
export { FAQBlock } from './FAQBlock'
export { TeamBlock } from './TeamBlock'
export { ServicesBlock } from './ServicesBlock'

View file

@ -17,6 +17,7 @@ import {
ProcessStepsBlock,
FAQBlock,
TeamBlock,
ServicesBlock,
} from '../blocks'
export const Pages: CollectionConfig = {
@ -101,6 +102,7 @@ export const Pages: CollectionConfig = {
ProcessStepsBlock,
FAQBlock,
TeamBlock,
ServicesBlock,
],
},
{

View file

@ -0,0 +1,102 @@
// src/collections/ServiceCategories.ts
import type { CollectionConfig } from 'payload'
import { authenticatedOnly, tenantScopedPublicRead } from '../lib/tenantAccess'
/**
* ServiceCategories Collection
*
* Kategorien für Dienstleistungen/Services.
* Ermöglicht Gruppierung und Filterung von Services.
* Tenant-scoped für Multi-Tenant-Betrieb.
*/
export const ServiceCategories: CollectionConfig = {
slug: 'service-categories',
admin: {
useAsTitle: 'name',
group: 'Content',
defaultColumns: ['name', 'slug', 'order', 'isActive'],
description: 'Kategorien für Leistungen/Services',
},
access: {
read: tenantScopedPublicRead,
create: authenticatedOnly,
update: authenticatedOnly,
delete: authenticatedOnly,
},
fields: [
{
name: 'name',
type: 'text',
required: true,
label: 'Name',
localized: true,
admin: {
description: 'z.B. "Pflege", "Beratung", "Schulung"',
},
},
{
name: 'slug',
type: 'text',
required: true,
label: 'Slug',
unique: false,
admin: {
description: 'URL-freundlicher Name (z.B. "pflege", "beratung")',
},
},
{
name: 'description',
type: 'textarea',
label: 'Beschreibung',
localized: true,
admin: {
description: 'Kurze Beschreibung der Kategorie',
},
},
{
name: 'icon',
type: 'text',
label: 'Icon',
admin: {
description: 'Icon-Name (z.B. "heart", "users", "book")',
},
},
{
name: 'image',
type: 'upload',
relationTo: 'media',
label: 'Bild',
admin: {
description: 'Optionales Kategorie-Bild',
},
},
{
name: 'color',
type: 'text',
label: 'Farbe',
admin: {
description: 'Akzentfarbe für die Kategorie (z.B. "#3B82F6")',
},
},
{
name: 'isActive',
type: 'checkbox',
defaultValue: true,
label: 'Aktiv',
admin: {
position: 'sidebar',
},
},
{
name: 'order',
type: 'number',
defaultValue: 0,
label: 'Sortierung',
admin: {
position: 'sidebar',
description: 'Niedrigere Zahlen werden zuerst angezeigt',
},
},
],
}

488
src/collections/Services.ts Normal file
View file

@ -0,0 +1,488 @@
// src/collections/Services.ts
import type { CollectionConfig } from 'payload'
import { authenticatedOnly, tenantScopedPublicRead } from '../lib/tenantAccess'
/**
* Services Collection
*
* Dienstleistungen und Leistungsangebote mit Detail-Seiten.
* Unterstützt Kategorisierung, Preisangaben und Verknüpfungen.
* Tenant-scoped für Multi-Tenant-Betrieb.
*/
export const Services: CollectionConfig = {
slug: 'services',
admin: {
useAsTitle: 'title',
group: 'Content',
defaultColumns: ['title', 'category', 'pricingType', 'order', 'isActive'],
description: 'Leistungen und Dienstleistungen',
},
access: {
read: tenantScopedPublicRead,
create: authenticatedOnly,
update: authenticatedOnly,
delete: authenticatedOnly,
},
fields: [
// Basis-Informationen
{
name: 'title',
type: 'text',
required: true,
label: 'Titel',
localized: true,
admin: {
description: 'Name der Leistung (z.B. "Intensivpflege", "Beratungsgespräch")',
},
},
{
name: 'slug',
type: 'text',
required: true,
label: 'Slug',
unique: false,
admin: {
description: 'URL-Pfad für Detail-Seite (z.B. "intensivpflege")',
},
},
{
name: 'subtitle',
type: 'text',
label: 'Untertitel',
localized: true,
admin: {
description: 'Optionaler Untertitel oder Slogan',
},
},
{
name: 'shortDescription',
type: 'textarea',
required: true,
label: 'Kurzbeschreibung',
localized: true,
admin: {
description: 'Kurze Beschreibung für Übersichtsseiten (1-2 Sätze)',
},
},
{
name: 'description',
type: 'richText',
label: 'Ausführliche Beschreibung',
localized: true,
admin: {
description: 'Detaillierte Beschreibung für die Detail-Seite',
},
},
// Medien
{
type: 'row',
fields: [
{
name: 'icon',
type: 'text',
label: 'Icon',
admin: {
width: '50%',
description: 'Icon-Name (z.B. "heart", "shield", "clock")',
},
},
{
name: 'iconImage',
type: 'upload',
relationTo: 'media',
label: 'Icon als Bild',
admin: {
width: '50%',
description: 'Alternativ: Icon als hochgeladenes Bild',
},
},
],
},
{
name: 'image',
type: 'upload',
relationTo: 'media',
label: 'Hauptbild',
admin: {
description: 'Bild für Karten und Header der Detail-Seite',
},
},
{
name: 'gallery',
type: 'array',
label: 'Bildergalerie',
admin: {
description: 'Zusätzliche Bilder für die Detail-Seite',
initCollapsed: true,
},
fields: [
{
name: 'image',
type: 'upload',
relationTo: 'media',
required: true,
label: 'Bild',
},
{
name: 'caption',
type: 'text',
label: 'Bildunterschrift',
localized: true,
},
],
},
// Kategorie
{
name: 'category',
type: 'relationship',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
relationTo: 'service-categories' as any,
label: 'Kategorie',
admin: {
description: 'Kategorie zur Gruppierung der Leistung',
},
},
// Features/Leistungsmerkmale
{
name: 'features',
type: 'array',
label: 'Leistungsmerkmale',
admin: {
description: 'Vorteile und Merkmale dieser Leistung',
},
fields: [
{
name: 'title',
type: 'text',
required: true,
label: 'Merkmal',
localized: true,
},
{
name: 'description',
type: 'textarea',
label: 'Beschreibung',
localized: true,
},
{
name: 'icon',
type: 'text',
label: 'Icon',
admin: {
description: 'Optionales Icon für das Merkmal',
},
},
],
},
// Preisgestaltung
{
type: 'collapsible',
label: 'Preisgestaltung',
admin: {
initCollapsed: false,
},
fields: [
{
name: 'pricingType',
type: 'select',
defaultValue: 'on-request',
label: 'Preistyp',
options: [
{ label: 'Auf Anfrage', value: 'on-request' },
{ label: 'Festpreis', value: 'fixed' },
{ label: 'Ab-Preis', value: 'from' },
{ label: 'Preisspanne', value: 'range' },
{ label: 'Stundensatz', value: 'hourly' },
{ label: 'Monatlich', value: 'monthly' },
{ label: 'Kostenlos', value: 'free' },
],
},
{
name: 'price',
type: 'number',
label: 'Preis (€)',
admin: {
condition: (data, siblingData) =>
['fixed', 'from', 'hourly', 'monthly'].includes(siblingData?.pricingType),
description: 'Preis in Euro',
},
},
{
name: 'priceMax',
type: 'number',
label: 'Preis bis (€)',
admin: {
condition: (data, siblingData) => siblingData?.pricingType === 'range',
description: 'Maximaler Preis bei Preisspanne',
},
},
{
name: 'priceUnit',
type: 'text',
label: 'Preiseinheit',
localized: true,
admin: {
condition: (data, siblingData) =>
['fixed', 'from', 'range', 'hourly', 'monthly'].includes(siblingData?.pricingType),
description: 'z.B. "pro Stunde", "pro Monat", "pro Behandlung"',
},
},
{
name: 'priceNote',
type: 'text',
label: 'Preishinweis',
localized: true,
admin: {
description: 'Zusätzliche Info (z.B. "zzgl. MwSt.", "inkl. Anfahrt")',
},
},
{
name: 'pricingDetails',
type: 'richText',
label: 'Preisdetails',
localized: true,
admin: {
description: 'Ausführliche Preisinformationen für Detail-Seite',
},
},
],
},
// Call-to-Action
{
type: 'collapsible',
label: 'Call-to-Action',
admin: {
initCollapsed: true,
},
fields: [
{
name: 'ctaText',
type: 'text',
defaultValue: 'Jetzt anfragen',
label: 'Button-Text',
localized: true,
},
{
name: 'ctaLink',
type: 'text',
defaultValue: '/kontakt',
label: 'Button-Link',
admin: {
description: 'Ziel-URL oder Pfad (z.B. "/kontakt", "#formular")',
},
},
{
name: 'ctaStyle',
type: 'select',
defaultValue: 'primary',
label: 'Button-Stil',
options: [
{ label: 'Primär', value: 'primary' },
{ label: 'Sekundär', value: 'secondary' },
{ label: 'Outline', value: 'outline' },
],
},
{
name: 'secondaryCta',
type: 'group',
label: 'Sekundärer Button',
fields: [
{
name: 'enabled',
type: 'checkbox',
defaultValue: false,
label: 'Aktivieren',
},
{
name: 'text',
type: 'text',
label: 'Text',
localized: true,
admin: {
condition: (data, siblingData) => siblingData?.enabled,
},
},
{
name: 'link',
type: 'text',
label: 'Link',
admin: {
condition: (data, siblingData) => siblingData?.enabled,
},
},
],
},
],
},
// Verknüpfungen
{
type: 'collapsible',
label: 'Verknüpfungen',
admin: {
initCollapsed: true,
},
fields: [
{
name: 'relatedServices',
type: 'relationship',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
relationTo: 'services' as any,
hasMany: true,
label: 'Verwandte Leistungen',
admin: {
description: 'Andere Leistungen die thematisch passen',
},
},
{
name: 'teamMembers',
type: 'relationship',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
relationTo: 'team' as any,
hasMany: true,
label: 'Zuständige Team-Mitglieder',
admin: {
description: 'Ansprechpartner für diese Leistung',
},
},
{
name: 'faqs',
type: 'relationship',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
relationTo: 'faqs' as any,
hasMany: true,
label: 'Häufige Fragen',
admin: {
description: 'FAQs zu dieser Leistung',
},
},
],
},
// Detail-Seiten-Inhalte
{
type: 'collapsible',
label: 'Detail-Seite',
admin: {
initCollapsed: true,
description: 'Zusätzliche Inhalte für die Detail-Seite',
},
fields: [
{
name: 'detailSections',
type: 'array',
label: 'Zusätzliche Abschnitte',
fields: [
{
name: 'title',
type: 'text',
required: true,
label: 'Überschrift',
localized: true,
},
{
name: 'content',
type: 'richText',
required: true,
label: 'Inhalt',
localized: true,
},
{
name: 'icon',
type: 'text',
label: 'Icon',
},
],
},
{
name: 'testimonialQuote',
type: 'textarea',
label: 'Kundenzitat',
localized: true,
admin: {
description: 'Optionales Zitat zur Leistung',
},
},
{
name: 'testimonialAuthor',
type: 'text',
label: 'Zitat-Autor',
},
],
},
// SEO
{
type: 'collapsible',
label: 'SEO',
admin: {
initCollapsed: true,
},
fields: [
{
name: 'metaTitle',
type: 'text',
label: 'Meta-Titel',
localized: true,
admin: {
description: 'Titel für Suchmaschinen (falls anders als Titel)',
},
},
{
name: 'metaDescription',
type: 'textarea',
label: 'Meta-Beschreibung',
localized: true,
admin: {
description: 'Beschreibung für Suchmaschinen',
},
},
{
name: 'ogImage',
type: 'upload',
relationTo: 'media',
label: 'Social Media Bild',
},
],
},
// Status & Sortierung
{
name: 'isActive',
type: 'checkbox',
defaultValue: true,
label: 'Aktiv/Sichtbar',
admin: {
position: 'sidebar',
description: 'Inaktive Leistungen werden nicht angezeigt',
},
},
{
name: 'isFeatured',
type: 'checkbox',
defaultValue: false,
label: 'Hervorgehoben',
admin: {
position: 'sidebar',
description: 'Für Startseiten-Anzeige oder besondere Hervorhebung',
},
},
{
name: 'isNew',
type: 'checkbox',
defaultValue: false,
label: 'Neu',
admin: {
position: 'sidebar',
description: 'Zeigt "Neu"-Badge an',
},
},
{
name: 'order',
type: 'number',
defaultValue: 0,
label: 'Sortierung',
admin: {
position: 'sidebar',
description: 'Niedrigere Zahlen werden zuerst angezeigt',
},
},
],
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,297 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
await db.execute(sql`
CREATE TYPE "public"."enum_pages_blocks_services_block_display_mode" AS ENUM('all', 'featured', 'category', 'selected');
CREATE TYPE "public"."enum_pages_blocks_services_block_layout" AS ENUM('grid', 'list', 'tabs', 'accordion', 'featured-grid', 'slider', 'compact', 'masonry');
CREATE TYPE "public"."enum_pages_blocks_services_block_columns" AS ENUM('2', '3', '4');
CREATE TYPE "public"."enum_pages_blocks_services_block_tabs_style" AS ENUM('horizontal', 'vertical', 'pills');
CREATE TYPE "public"."enum_pages_blocks_services_block_slides_per_view" AS ENUM('1', '2', '3', '4');
CREATE TYPE "public"."enum_pages_blocks_services_block_card_style" AS ENUM('elevated', 'bordered', 'flat', 'filled');
CREATE TYPE "public"."enum_pages_blocks_services_block_background_color" AS ENUM('white', 'light', 'dark', 'accent');
CREATE TYPE "public"."enum_services_pricing_type" AS ENUM('on-request', 'fixed', 'from', 'range', 'hourly', 'monthly', 'free');
CREATE TYPE "public"."enum_services_cta_style" AS ENUM('primary', 'secondary', 'outline');
CREATE TABLE "pages_blocks_services_block" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"display_mode" "enum_pages_blocks_services_block_display_mode" DEFAULT 'all',
"category_id" integer,
"limit" numeric DEFAULT 12,
"layout" "enum_pages_blocks_services_block_layout" DEFAULT 'grid',
"columns" "enum_pages_blocks_services_block_columns" DEFAULT '3',
"featured_count" numeric DEFAULT 1,
"tabs_style" "enum_pages_blocks_services_block_tabs_style" DEFAULT 'horizontal',
"show_all_tab" boolean DEFAULT true,
"expand_first" boolean DEFAULT true,
"allow_multiple_open" boolean DEFAULT false,
"autoplay" boolean DEFAULT true,
"autoplay_speed" numeric DEFAULT 5000,
"slides_per_view" "enum_pages_blocks_services_block_slides_per_view" DEFAULT '3',
"show_image" boolean DEFAULT true,
"show_icon" boolean DEFAULT true,
"show_description" boolean DEFAULT true,
"show_category" boolean DEFAULT false,
"show_pricing" boolean DEFAULT true,
"show_features" boolean DEFAULT false,
"features_limit" numeric DEFAULT 3,
"show_c_t_a" boolean DEFAULT true,
"show_new_badge" boolean DEFAULT true,
"group_by_category" boolean DEFAULT false,
"link_to_detail" boolean DEFAULT true,
"services_base_path" varchar DEFAULT '/leistungen',
"card_style" "enum_pages_blocks_services_block_card_style" DEFAULT 'elevated',
"background_color" "enum_pages_blocks_services_block_background_color" DEFAULT 'white',
"show_section_c_t_a" boolean DEFAULT false,
"section_c_t_a_link" varchar DEFAULT '/leistungen',
"block_name" varchar
);
CREATE TABLE "pages_blocks_services_block_locales" (
"title" varchar DEFAULT 'Unsere Leistungen',
"subtitle" varchar,
"introduction" jsonb,
"detail_link_text" varchar DEFAULT 'Mehr erfahren',
"section_c_t_a_text" varchar DEFAULT 'Alle Leistungen ansehen',
"id" serial PRIMARY KEY NOT NULL,
"_locale" "_locales" NOT NULL,
"_parent_id" varchar NOT NULL
);
CREATE TABLE "service_categories" (
"id" serial PRIMARY KEY NOT NULL,
"tenant_id" integer,
"slug" varchar NOT NULL,
"icon" varchar,
"image_id" integer,
"color" varchar,
"is_active" boolean DEFAULT true,
"order" numeric DEFAULT 0,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
CREATE TABLE "service_categories_locales" (
"name" varchar NOT NULL,
"description" varchar,
"id" serial PRIMARY KEY NOT NULL,
"_locale" "_locales" NOT NULL,
"_parent_id" integer NOT NULL
);
CREATE TABLE "services_gallery" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"image_id" integer NOT NULL
);
CREATE TABLE "services_gallery_locales" (
"caption" varchar,
"id" serial PRIMARY KEY NOT NULL,
"_locale" "_locales" NOT NULL,
"_parent_id" varchar NOT NULL
);
CREATE TABLE "services_features" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"icon" varchar
);
CREATE TABLE "services_features_locales" (
"title" varchar NOT NULL,
"description" varchar,
"id" serial PRIMARY KEY NOT NULL,
"_locale" "_locales" NOT NULL,
"_parent_id" varchar NOT NULL
);
CREATE TABLE "services_detail_sections" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"icon" varchar
);
CREATE TABLE "services_detail_sections_locales" (
"title" varchar NOT NULL,
"content" jsonb NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"_locale" "_locales" NOT NULL,
"_parent_id" varchar NOT NULL
);
CREATE TABLE "services" (
"id" serial PRIMARY KEY NOT NULL,
"tenant_id" integer,
"slug" varchar NOT NULL,
"icon" varchar,
"icon_image_id" integer,
"image_id" integer,
"category_id" integer,
"pricing_type" "enum_services_pricing_type" DEFAULT 'on-request',
"price" numeric,
"price_max" numeric,
"cta_link" varchar DEFAULT '/kontakt',
"cta_style" "enum_services_cta_style" DEFAULT 'primary',
"secondary_cta_enabled" boolean DEFAULT false,
"secondary_cta_link" varchar,
"testimonial_author" varchar,
"og_image_id" integer,
"is_active" boolean DEFAULT true,
"is_featured" boolean DEFAULT false,
"is_new" boolean DEFAULT false,
"order" numeric DEFAULT 0,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
CREATE TABLE "services_locales" (
"title" varchar NOT NULL,
"subtitle" varchar,
"short_description" varchar NOT NULL,
"description" jsonb,
"price_unit" varchar,
"price_note" varchar,
"pricing_details" jsonb,
"cta_text" varchar DEFAULT 'Jetzt anfragen',
"secondary_cta_text" varchar,
"testimonial_quote" varchar,
"meta_title" varchar,
"meta_description" varchar,
"id" serial PRIMARY KEY NOT NULL,
"_locale" "_locales" NOT NULL,
"_parent_id" integer NOT NULL
);
CREATE TABLE "services_rels" (
"id" serial PRIMARY KEY NOT NULL,
"order" integer,
"parent_id" integer NOT NULL,
"path" varchar NOT NULL,
"services_id" integer,
"team_id" integer,
"faqs_id" integer
);
ALTER TABLE "pages_rels" ADD COLUMN "services_id" integer;
ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "service_categories_id" integer;
ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "services_id" integer;
ALTER TABLE "pages_blocks_services_block" ADD CONSTRAINT "pages_blocks_services_block_category_id_service_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "public"."service_categories"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "pages_blocks_services_block" ADD CONSTRAINT "pages_blocks_services_block_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_services_block_locales" ADD CONSTRAINT "pages_blocks_services_block_locales_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."pages_blocks_services_block"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "service_categories" ADD CONSTRAINT "service_categories_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "service_categories" ADD CONSTRAINT "service_categories_image_id_media_id_fk" FOREIGN KEY ("image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "service_categories_locales" ADD CONSTRAINT "service_categories_locales_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."service_categories"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "services_gallery" ADD CONSTRAINT "services_gallery_image_id_media_id_fk" FOREIGN KEY ("image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "services_gallery" ADD CONSTRAINT "services_gallery_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."services"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "services_gallery_locales" ADD CONSTRAINT "services_gallery_locales_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."services_gallery"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "services_features" ADD CONSTRAINT "services_features_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."services"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "services_features_locales" ADD CONSTRAINT "services_features_locales_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."services_features"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "services_detail_sections" ADD CONSTRAINT "services_detail_sections_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."services"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "services_detail_sections_locales" ADD CONSTRAINT "services_detail_sections_locales_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."services_detail_sections"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "services" ADD CONSTRAINT "services_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "services" ADD CONSTRAINT "services_icon_image_id_media_id_fk" FOREIGN KEY ("icon_image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "services" ADD CONSTRAINT "services_image_id_media_id_fk" FOREIGN KEY ("image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "services" ADD CONSTRAINT "services_category_id_service_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "public"."service_categories"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "services" ADD CONSTRAINT "services_og_image_id_media_id_fk" FOREIGN KEY ("og_image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "services_locales" ADD CONSTRAINT "services_locales_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."services"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "services_rels" ADD CONSTRAINT "services_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."services"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "services_rels" ADD CONSTRAINT "services_rels_services_fk" FOREIGN KEY ("services_id") REFERENCES "public"."services"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "services_rels" ADD CONSTRAINT "services_rels_team_fk" FOREIGN KEY ("team_id") REFERENCES "public"."team"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "services_rels" ADD CONSTRAINT "services_rels_faqs_fk" FOREIGN KEY ("faqs_id") REFERENCES "public"."faqs"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX "pages_blocks_services_block_order_idx" ON "pages_blocks_services_block" USING btree ("_order");
CREATE INDEX "pages_blocks_services_block_parent_id_idx" ON "pages_blocks_services_block" USING btree ("_parent_id");
CREATE INDEX "pages_blocks_services_block_path_idx" ON "pages_blocks_services_block" USING btree ("_path");
CREATE INDEX "pages_blocks_services_block_category_idx" ON "pages_blocks_services_block" USING btree ("category_id");
CREATE UNIQUE INDEX "pages_blocks_services_block_locales_locale_parent_id_unique" ON "pages_blocks_services_block_locales" USING btree ("_locale","_parent_id");
CREATE INDEX "service_categories_tenant_idx" ON "service_categories" USING btree ("tenant_id");
CREATE INDEX "service_categories_image_idx" ON "service_categories" USING btree ("image_id");
CREATE INDEX "service_categories_updated_at_idx" ON "service_categories" USING btree ("updated_at");
CREATE INDEX "service_categories_created_at_idx" ON "service_categories" USING btree ("created_at");
CREATE UNIQUE INDEX "service_categories_locales_locale_parent_id_unique" ON "service_categories_locales" USING btree ("_locale","_parent_id");
CREATE INDEX "services_gallery_order_idx" ON "services_gallery" USING btree ("_order");
CREATE INDEX "services_gallery_parent_id_idx" ON "services_gallery" USING btree ("_parent_id");
CREATE INDEX "services_gallery_image_idx" ON "services_gallery" USING btree ("image_id");
CREATE UNIQUE INDEX "services_gallery_locales_locale_parent_id_unique" ON "services_gallery_locales" USING btree ("_locale","_parent_id");
CREATE INDEX "services_features_order_idx" ON "services_features" USING btree ("_order");
CREATE INDEX "services_features_parent_id_idx" ON "services_features" USING btree ("_parent_id");
CREATE UNIQUE INDEX "services_features_locales_locale_parent_id_unique" ON "services_features_locales" USING btree ("_locale","_parent_id");
CREATE INDEX "services_detail_sections_order_idx" ON "services_detail_sections" USING btree ("_order");
CREATE INDEX "services_detail_sections_parent_id_idx" ON "services_detail_sections" USING btree ("_parent_id");
CREATE UNIQUE INDEX "services_detail_sections_locales_locale_parent_id_unique" ON "services_detail_sections_locales" USING btree ("_locale","_parent_id");
CREATE INDEX "services_tenant_idx" ON "services" USING btree ("tenant_id");
CREATE INDEX "services_icon_image_idx" ON "services" USING btree ("icon_image_id");
CREATE INDEX "services_image_idx" ON "services" USING btree ("image_id");
CREATE INDEX "services_category_idx" ON "services" USING btree ("category_id");
CREATE INDEX "services_og_image_idx" ON "services" USING btree ("og_image_id");
CREATE INDEX "services_updated_at_idx" ON "services" USING btree ("updated_at");
CREATE INDEX "services_created_at_idx" ON "services" USING btree ("created_at");
CREATE UNIQUE INDEX "services_locales_locale_parent_id_unique" ON "services_locales" USING btree ("_locale","_parent_id");
CREATE INDEX "services_rels_order_idx" ON "services_rels" USING btree ("order");
CREATE INDEX "services_rels_parent_idx" ON "services_rels" USING btree ("parent_id");
CREATE INDEX "services_rels_path_idx" ON "services_rels" USING btree ("path");
CREATE INDEX "services_rels_services_id_idx" ON "services_rels" USING btree ("services_id");
CREATE INDEX "services_rels_team_id_idx" ON "services_rels" USING btree ("team_id");
CREATE INDEX "services_rels_faqs_id_idx" ON "services_rels" USING btree ("faqs_id");
ALTER TABLE "pages_rels" ADD CONSTRAINT "pages_rels_services_fk" FOREIGN KEY ("services_id") REFERENCES "public"."services"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_service_categories_fk" FOREIGN KEY ("service_categories_id") REFERENCES "public"."service_categories"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_services_fk" FOREIGN KEY ("services_id") REFERENCES "public"."services"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX "pages_rels_services_id_idx" ON "pages_rels" USING btree ("services_id");
CREATE INDEX "payload_locked_documents_rels_service_categories_id_idx" ON "payload_locked_documents_rels" USING btree ("service_categories_id");
CREATE INDEX "payload_locked_documents_rels_services_id_idx" ON "payload_locked_documents_rels" USING btree ("services_id");`)
}
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
ALTER TABLE "pages_blocks_services_block" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "pages_blocks_services_block_locales" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "service_categories" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "service_categories_locales" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "services_gallery" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "services_gallery_locales" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "services_features" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "services_features_locales" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "services_detail_sections" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "services_detail_sections_locales" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "services" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "services_locales" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "services_rels" DISABLE ROW LEVEL SECURITY;
DROP TABLE "pages_blocks_services_block" CASCADE;
DROP TABLE "pages_blocks_services_block_locales" CASCADE;
DROP TABLE "service_categories" CASCADE;
DROP TABLE "service_categories_locales" CASCADE;
DROP TABLE "services_gallery" CASCADE;
DROP TABLE "services_gallery_locales" CASCADE;
DROP TABLE "services_features" CASCADE;
DROP TABLE "services_features_locales" CASCADE;
DROP TABLE "services_detail_sections" CASCADE;
DROP TABLE "services_detail_sections_locales" CASCADE;
DROP TABLE "services" CASCADE;
DROP TABLE "services_locales" CASCADE;
DROP TABLE "services_rels" CASCADE;
ALTER TABLE "pages_rels" DROP CONSTRAINT "pages_rels_services_fk";
ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_service_categories_fk";
ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_services_fk";
DROP INDEX "pages_rels_services_id_idx";
DROP INDEX "payload_locked_documents_rels_service_categories_id_idx";
DROP INDEX "payload_locked_documents_rels_services_id_idx";
ALTER TABLE "pages_rels" DROP COLUMN "services_id";
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "service_categories_id";
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "services_id";
DROP TYPE "public"."enum_pages_blocks_services_block_display_mode";
DROP TYPE "public"."enum_pages_blocks_services_block_layout";
DROP TYPE "public"."enum_pages_blocks_services_block_columns";
DROP TYPE "public"."enum_pages_blocks_services_block_tabs_style";
DROP TYPE "public"."enum_pages_blocks_services_block_slides_per_view";
DROP TYPE "public"."enum_pages_blocks_services_block_card_style";
DROP TYPE "public"."enum_pages_blocks_services_block_background_color";
DROP TYPE "public"."enum_services_pricing_type";
DROP TYPE "public"."enum_services_cta_style";`)
}

View file

@ -6,6 +6,7 @@ import * as migration_20251206_141403_email_logs_collection from './20251206_141
import * as migration_20251207_205727_audit_logs_collection from './20251207_205727_audit_logs_collection';
import * as migration_20251210_052757_add_faqs_collection from './20251210_052757_add_faqs_collection';
import * as migration_20251210_071506_add_team_collection from './20251210_071506_add_team_collection';
import * as migration_20251210_073811_add_services_collections from './20251210_073811_add_services_collections';
export const migrations = [
{
@ -46,6 +47,11 @@ export const migrations = [
{
up: migration_20251210_071506_add_team_collection.up,
down: migration_20251210_071506_add_team_collection.down,
name: '20251210_071506_add_team_collection'
name: '20251210_071506_add_team_collection',
},
{
up: migration_20251210_073811_add_services_collections.up,
down: migration_20251210_073811_add_services_collections.down,
name: '20251210_073811_add_services_collections'
},
];

View file

@ -25,6 +25,8 @@ import { SocialLinks } from './collections/SocialLinks'
import { Testimonials } from './collections/Testimonials'
import { FAQs } from './collections/FAQs'
import { Team } from './collections/Team'
import { ServiceCategories } from './collections/ServiceCategories'
import { Services } from './collections/Services'
import { NewsletterSubscribers } from './collections/NewsletterSubscribers'
// Portfolio Collections
@ -132,6 +134,8 @@ export default buildConfig({
Testimonials,
FAQs,
Team,
ServiceCategories,
Services,
NewsletterSubscribers,
// Portfolio
PortfolioCategories,
@ -175,6 +179,8 @@ export default buildConfig({
testimonials: {},
faqs: {},
team: {},
'service-categories': {},
services: {},
'newsletter-subscribers': {},
// Portfolio Collections
'portfolio-categories': {},