feat: add priority collections and advanced content blocks

New Collections:
- Events: Veranstaltungen mit Datum, Ort, Registrierung
- Jobs: Stellenangebote mit Standort und Bewerbungsfrist
- Locations: Standorte mit Adresse, Kontakt, Öffnungszeiten
- Partners: Partner/Kunden mit Logo und Beschreibung
- Downloads: Dateien mit Kategorisierung und Tracking

New Blocks:
- EventsBlock: Veranstaltungslisten mit Kalender-Ansicht
- JobsBlock: Stellenanzeigen mit Filterfunktion
- LocationsBlock: Standort-Karten und Listen
- PricingBlock: Preistabellen mit Feature-Vergleich
- TabsBlock: Tabbed Content mit verschiedenen Stilen
- AccordionBlock: FAQ/Accordion mit Animationen
- ComparisonBlock: Vergleichstabellen (Tabelle, Karten, Pro/Contra)
- StatsBlock: Statistiken mit Counter-Animation
- LogoGridBlock: Logo-Wolken und Partner-Galerien
- MapBlock: Interaktive Karten mit Markern
- DownloadsBlock: Download-Listen mit Kategorien

All collections support multi-tenant isolation and localization.

🤖 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-14 00:58:30 +00:00
parent 2b097eefb3
commit 9e5c741941
23 changed files with 23275 additions and 536 deletions

View file

@ -0,0 +1,400 @@
import type { Block } from 'payload'
/**
* Accordion Block
*
* Aufklappbare Inhaltsbereiche für FAQs, Dokumentation,
* Produktdetails etc. Mehr Flexibilität als der FAQ-Block.
*/
export const AccordionBlock: Block = {
slug: 'accordion',
labels: {
singular: 'Accordion',
plural: 'Accordions',
},
fields: [
{
name: 'title',
type: 'text',
label: 'Überschrift',
localized: true,
},
{
name: 'subtitle',
type: 'text',
label: 'Untertitel',
localized: true,
},
{
name: 'description',
type: 'textarea',
label: 'Beschreibung',
localized: true,
},
// Accordion Items
{
name: 'items',
type: 'array',
label: 'Einträge',
minRows: 1,
fields: [
{
name: 'title',
type: 'text',
required: true,
label: 'Titel',
localized: true,
},
{
name: 'subtitle',
type: 'text',
label: 'Untertitel',
localized: true,
},
{
name: 'icon',
type: 'select',
label: 'Icon',
options: [
{ label: 'Keines', value: 'none' },
{ label: 'Info', value: 'info' },
{ label: 'Frage', value: 'question' },
{ label: 'Stern', value: 'star' },
{ label: 'Häkchen', value: 'check' },
{ label: 'Warnung', value: 'warning' },
{ label: 'Dokument', value: 'document' },
{ label: 'Benutzer', value: 'user' },
{ label: 'Einstellungen', value: 'settings' },
{ label: 'Code', value: 'code' },
{ label: 'Lampe', value: 'lightbulb' },
{ label: 'Schloss', value: 'lock' },
],
},
{
name: 'customIcon',
type: 'upload',
relationTo: 'media',
label: 'Eigenes Icon',
},
// Content
{
name: 'contentType',
type: 'select',
defaultValue: 'richtext',
label: 'Content-Typ',
options: [
{ label: 'Rich Text', value: 'richtext' },
{ label: 'Bild & Text', value: 'image-text' },
{ label: 'Liste', value: 'list' },
{ label: 'Tabelle', value: 'table' },
{ label: 'Code', value: 'code' },
],
},
// Rich Text
{
name: 'content',
type: 'richText',
label: 'Inhalt',
localized: true,
admin: {
condition: (data, siblingData) => siblingData?.contentType === 'richtext',
},
},
// Image & Text
{
name: 'imgTxt',
type: 'group',
label: 'Bild & Text',
admin: {
condition: (data, siblingData) => siblingData?.contentType === 'image-text',
},
fields: [
{
name: 'img',
type: 'upload',
relationTo: 'media',
label: 'Bild',
},
{
name: 'imgPos',
type: 'select',
defaultValue: 'left',
label: 'Bild-Position',
options: [
{ label: 'Links', value: 'left' },
{ label: 'Rechts', value: 'right' },
],
},
{
name: 'text',
type: 'richText',
label: 'Text',
localized: true,
},
],
},
// List
{
name: 'listItems',
type: 'array',
label: 'Listen-Einträge',
admin: {
condition: (data, siblingData) => siblingData?.contentType === 'list',
},
fields: [
{
name: 'text',
type: 'text',
required: true,
label: 'Text',
localized: true,
},
{
name: 'icon',
type: 'select',
defaultValue: 'check',
label: 'Icon',
options: [
{ label: 'Häkchen', value: 'check' },
{ label: 'Punkt', value: 'dot' },
{ label: 'Pfeil', value: 'arrow' },
{ label: 'Stern', value: 'star' },
{ label: 'X', value: 'x' },
],
},
],
},
// Table
{
name: 'tableData',
type: 'group',
label: 'Tabelle',
admin: {
condition: (data, siblingData) => siblingData?.contentType === 'table',
},
fields: [
{
name: 'headers',
type: 'array',
label: 'Spaltenüberschriften',
fields: [
{
name: 'text',
type: 'text',
label: 'Text',
localized: true,
},
],
},
{
name: 'rows',
type: 'array',
label: 'Zeilen',
fields: [
{
name: 'cells',
type: 'array',
label: 'Zellen',
fields: [
{
name: 'text',
type: 'text',
label: 'Text',
localized: true,
},
],
},
],
},
],
},
// Code
{
name: 'codeContent',
type: 'group',
label: 'Code',
admin: {
condition: (data, siblingData) => siblingData?.contentType === 'code',
},
fields: [
{
name: 'language',
type: 'select',
label: 'Sprache',
options: [
{ label: 'JavaScript', value: 'javascript' },
{ label: 'TypeScript', value: 'typescript' },
{ label: 'HTML', value: 'html' },
{ label: 'CSS', value: 'css' },
{ label: 'JSON', value: 'json' },
{ label: 'Python', value: 'python' },
{ label: 'Bash', value: 'bash' },
],
},
{
name: 'code',
type: 'code',
label: 'Code',
},
],
},
// Status/Badge
{
name: 'badge',
type: 'text',
label: 'Badge',
admin: {
description: 'z.B. "Neu", "Wichtig", "Pro"',
},
},
{
name: 'badgeColor',
type: 'select',
label: 'Badge-Farbe',
options: [
{ label: 'Grau', value: 'gray' },
{ label: 'Blau', value: 'blue' },
{ label: 'Grün', value: 'green' },
{ label: 'Gelb', value: 'yellow' },
{ label: 'Rot', value: 'red' },
{ label: 'Lila', value: 'purple' },
],
admin: {
condition: (data, siblingData) => siblingData?.badge,
},
},
// State
{
name: 'defaultOpen',
type: 'checkbox',
defaultValue: false,
label: 'Standardmäßig geöffnet',
},
{
name: 'disabled',
type: 'checkbox',
defaultValue: false,
label: 'Deaktiviert',
},
],
},
// Verhalten
{
name: 'behavior',
type: 'select',
defaultValue: 'single',
label: 'Verhalten',
options: [
{ label: 'Nur eines offen (klassisch)', value: 'single' },
{ label: 'Mehrere gleichzeitig', value: 'multiple' },
{ label: 'Mindestens eines offen', value: 'at-least-one' },
],
},
{
name: 'expandFirst',
type: 'checkbox',
defaultValue: false,
label: 'Erstes Element standardmäßig öffnen',
},
{
name: 'animated',
type: 'checkbox',
defaultValue: true,
label: 'Animierter Übergang',
},
// Stil
{
name: 'style',
type: 'select',
defaultValue: 'default',
label: 'Stil',
options: [
{ label: 'Standard', value: 'default' },
{ label: 'Bordered', value: 'bordered' },
{ label: 'Separated', value: 'separated' },
{ label: 'Flush', value: 'flush' },
{ label: 'Rounded', value: 'rounded' },
],
},
{
name: 'iconPosition',
type: 'select',
defaultValue: 'right',
label: 'Pfeil-Position',
options: [
{ label: 'Links', value: 'left' },
{ label: 'Rechts', value: 'right' },
],
},
{
name: 'iconStyle',
type: 'select',
defaultValue: 'chevron',
label: 'Pfeil-Stil',
options: [
{ label: 'Chevron (>)', value: 'chevron' },
{ label: 'Plus/Minus', value: 'plus-minus' },
{ label: 'Pfeil', value: 'arrow' },
{ label: 'Caret', value: 'caret' },
],
},
// Layout
{
name: 'layout',
type: 'select',
defaultValue: 'full',
label: 'Layout',
options: [
{ label: 'Volle Breite', value: 'full' },
{ label: 'Zentriert', value: 'centered' },
{ label: 'Schmal', value: 'narrow' },
{ label: 'Zweispaltig', value: 'two-column' },
],
},
{
name: 'titleSize',
type: 'select',
defaultValue: 'medium',
label: 'Titel-Größe',
options: [
{ label: 'Klein', value: 'small' },
{ label: 'Mittel', value: 'medium' },
{ label: 'Groß', value: 'large' },
],
},
// Styling
{
name: 'backgroundColor',
type: 'select',
defaultValue: 'white',
label: 'Hintergrund',
options: [
{ label: 'Weiß', value: 'white' },
{ label: 'Hell (Grau)', value: 'light' },
{ label: 'Dunkel', value: 'dark' },
{ label: 'Transparent', value: 'transparent' },
],
},
{
name: 'headerBackground',
type: 'select',
defaultValue: 'white',
label: 'Header-Hintergrund',
options: [
{ label: 'Weiß', value: 'white' },
{ label: 'Hell (Grau)', value: 'light' },
{ label: 'Transparent', value: 'transparent' },
],
},
// Schema.org
{
name: 'enableSchemaOrg',
type: 'checkbox',
defaultValue: false,
label: 'Schema.org FAQ generieren',
admin: {
description: 'Wenn aktiviert, wird FAQ Structured Data generiert',
},
},
],
}

View file

@ -0,0 +1,598 @@
import type { Block } from 'payload'
/**
* Comparison Block
*
* Vergleichstabellen für Produkte, Dienstleistungen, Pläne etc.
* Unterstützt verschiedene Layouts und Darstellungsformen.
*/
export const ComparisonBlock: Block = {
slug: 'comparison',
labels: {
singular: 'Vergleich',
plural: 'Vergleiche',
},
fields: [
{
name: 'title',
type: 'text',
label: 'Überschrift',
localized: true,
},
{
name: 'subtitle',
type: 'text',
label: 'Untertitel',
localized: true,
},
{
name: 'description',
type: 'textarea',
label: 'Beschreibung',
localized: true,
},
// Vergleichstyp
{
name: 'comparisonType',
type: 'select',
defaultValue: 'table',
label: 'Vergleichs-Typ',
options: [
{ label: 'Tabelle', value: 'table' },
{ label: 'Karten nebeneinander', value: 'cards' },
{ label: 'Vorher/Nachher', value: 'before-after' },
{ label: 'Pro/Contra', value: 'pros-cons' },
{ label: 'Feature-Matrix', value: 'feature-matrix' },
],
},
// Tabellen-Vergleich
{
name: 'tbl',
type: 'group',
label: 'Tabellen-Vergleich',
admin: {
condition: (data, siblingData) =>
siblingData?.comparisonType === 'table' ||
siblingData?.comparisonType === 'feature-matrix',
},
fields: [
// Spalten (Produkte/Optionen)
{
name: 'columns',
type: 'array',
label: 'Spalten (Optionen)',
minRows: 2,
maxRows: 6,
fields: [
{
name: 'name',
type: 'text',
required: true,
label: 'Name',
localized: true,
},
{
name: 'subtitle',
type: 'text',
label: 'Untertitel',
localized: true,
},
{
name: 'image',
type: 'upload',
relationTo: 'media',
label: 'Bild/Logo',
},
{
name: 'price',
type: 'text',
label: 'Preis',
localized: true,
},
{
name: 'isHighlighted',
type: 'checkbox',
defaultValue: false,
label: 'Hervorheben',
},
{
name: 'highlightLabel',
type: 'text',
label: 'Highlight-Label',
localized: true,
admin: {
condition: (data, siblingData) => siblingData?.isHighlighted,
description: 'z.B. "Empfohlen", "Bestseller"',
},
},
{
name: 'ctaText',
type: 'text',
label: 'Button-Text',
localized: true,
},
{
name: 'ctaLink',
type: 'text',
label: 'Button-Link',
},
],
},
// Zeilen (Features)
{
name: 'rows',
type: 'array',
label: 'Zeilen (Features)',
fields: [
{
name: 'feature',
type: 'text',
required: true,
label: 'Feature',
localized: true,
},
{
name: 'category',
type: 'text',
label: 'Kategorie',
admin: {
description: 'Für Gruppierung',
},
},
{
name: 'tooltip',
type: 'text',
label: 'Tooltip',
localized: true,
},
{
name: 'values',
type: 'array',
label: 'Werte',
fields: [
{
name: 'columnIndex',
type: 'number',
label: 'Spalten-Index',
admin: {
description: '0 = erste Spalte',
},
},
{
name: 'valueType',
type: 'select',
defaultValue: 'text',
label: 'Wert-Typ',
options: [
{ label: 'Text', value: 'text' },
{ label: 'Ja/Nein', value: 'boolean' },
{ label: 'Teilweise', value: 'partial' },
{ label: 'Nicht verfügbar', value: 'na' },
],
},
{
name: 'textValue',
type: 'text',
label: 'Text-Wert',
localized: true,
admin: {
condition: (data, siblingData) => siblingData?.valueType === 'text',
},
},
{
name: 'booleanValue',
type: 'checkbox',
label: 'Ja/Nein',
admin: {
condition: (data, siblingData) => siblingData?.valueType === 'boolean',
},
},
{
name: 'partialNote',
type: 'text',
label: 'Hinweis',
localized: true,
admin: {
condition: (data, siblingData) => siblingData?.valueType === 'partial',
description: 'z.B. "Eingeschränkt", "Mit Aufpreis"',
},
},
],
},
],
},
],
},
// Karten-Vergleich
{
name: 'crd',
type: 'group',
label: 'Karten-Vergleich',
admin: {
condition: (data, siblingData) => siblingData?.comparisonType === 'cards',
},
fields: [
{
name: 'items',
type: 'array',
label: 'Karten',
minRows: 2,
maxRows: 4,
fields: [
{
name: 'title',
type: 'text',
required: true,
label: 'Titel',
localized: true,
},
{
name: 'subtitle',
type: 'text',
label: 'Untertitel',
localized: true,
},
{
name: 'image',
type: 'upload',
relationTo: 'media',
label: 'Bild',
},
{
name: 'description',
type: 'richText',
label: 'Beschreibung',
localized: true,
},
{
name: 'features',
type: 'array',
label: 'Features',
fields: [
{
name: 'text',
type: 'text',
required: true,
localized: true,
},
{
name: 'included',
type: 'checkbox',
defaultValue: true,
},
],
},
{
name: 'price',
type: 'text',
label: 'Preis',
localized: true,
},
{
name: 'ctaText',
type: 'text',
label: 'Button-Text',
localized: true,
},
{
name: 'ctaLink',
type: 'text',
label: 'Button-Link',
},
{
name: 'isHighlighted',
type: 'checkbox',
defaultValue: false,
label: 'Hervorheben',
},
{
name: 'accentColor',
type: 'select',
label: 'Akzentfarbe',
options: [
{ label: 'Standard', value: 'default' },
{ label: 'Primär', value: 'primary' },
{ label: 'Sekundär', value: 'secondary' },
{ label: 'Grün', value: 'green' },
{ label: 'Blau', value: 'blue' },
],
},
],
},
],
},
// Vorher/Nachher
{
name: 'beforeAfter',
type: 'group',
label: 'Vorher/Nachher',
admin: {
condition: (data, siblingData) => siblingData?.comparisonType === 'before-after',
},
fields: [
{
name: 'beforeLabel',
type: 'text',
defaultValue: 'Vorher',
label: 'Vorher-Label',
localized: true,
},
{
name: 'afterLabel',
type: 'text',
defaultValue: 'Nachher',
label: 'Nachher-Label',
localized: true,
},
{
name: 'items',
type: 'array',
label: 'Vergleiche',
fields: [
{
name: 'title',
type: 'text',
label: 'Titel',
localized: true,
},
{
name: 'beforeImage',
type: 'upload',
relationTo: 'media',
label: 'Vorher-Bild',
},
{
name: 'afterImage',
type: 'upload',
relationTo: 'media',
label: 'Nachher-Bild',
},
{
name: 'beforeText',
type: 'textarea',
label: 'Vorher-Text',
localized: true,
},
{
name: 'afterText',
type: 'textarea',
label: 'Nachher-Text',
localized: true,
},
],
},
{
name: 'displayStyle',
type: 'select',
defaultValue: 'slider',
label: 'Darstellung',
options: [
{ label: 'Slider (Schieberegler)', value: 'slider' },
{ label: 'Nebeneinander', value: 'side-by-side' },
{ label: 'Übereinander', value: 'stacked' },
{ label: 'Flip', value: 'flip' },
],
},
],
},
// Pro/Contra
{
name: 'prosCons',
type: 'group',
label: 'Pro/Contra',
admin: {
condition: (data, siblingData) => siblingData?.comparisonType === 'pros-cons',
},
fields: [
{
name: 'prosLabel',
type: 'text',
defaultValue: 'Vorteile',
label: 'Vorteile-Label',
localized: true,
},
{
name: 'consLabel',
type: 'text',
defaultValue: 'Nachteile',
label: 'Nachteile-Label',
localized: true,
},
{
name: 'items',
type: 'array',
label: 'Einträge',
fields: [
{
name: 'title',
type: 'text',
label: 'Titel',
localized: true,
},
{
name: 'image',
type: 'upload',
relationTo: 'media',
label: 'Bild',
},
{
name: 'pros',
type: 'array',
label: 'Vorteile',
fields: [
{
name: 'text',
type: 'text',
required: true,
localized: true,
},
],
},
{
name: 'cons',
type: 'array',
label: 'Nachteile',
fields: [
{
name: 'text',
type: 'text',
required: true,
localized: true,
},
],
},
{
name: 'verdict',
type: 'textarea',
label: 'Fazit',
localized: true,
},
{
name: 'rating',
type: 'number',
min: 0,
max: 5,
label: 'Bewertung (0-5)',
},
],
},
],
},
// Layout-Optionen
{
name: 'stickyHeader',
type: 'checkbox',
defaultValue: true,
label: 'Sticky Header',
admin: {
condition: (data, siblingData) =>
siblingData?.comparisonType === 'table' ||
siblingData?.comparisonType === 'feature-matrix',
},
},
{
name: 'showCategories',
type: 'checkbox',
defaultValue: true,
label: 'Kategorien anzeigen',
admin: {
condition: (data, siblingData) =>
siblingData?.comparisonType === 'table' ||
siblingData?.comparisonType === 'feature-matrix',
},
},
{
name: 'collapsibleCategories',
type: 'checkbox',
defaultValue: false,
label: 'Kategorien einklappbar',
admin: {
condition: (data, siblingData) =>
siblingData?.comparisonType === 'table' ||
siblingData?.comparisonType === 'feature-matrix',
},
},
{
name: 'showRowDividers',
type: 'checkbox',
defaultValue: true,
label: 'Zeilen-Trennlinien',
},
{
name: 'highlightDifferences',
type: 'checkbox',
defaultValue: false,
label: 'Unterschiede hervorheben',
},
// Mobile
{
name: 'mobileView',
type: 'select',
defaultValue: 'scroll',
label: 'Mobile-Ansicht',
options: [
{ label: 'Horizontal scrollen', value: 'scroll' },
{ label: 'Karten gestapelt', value: 'stacked' },
{ label: 'Accordion', value: 'accordion' },
{ label: 'Dropdown-Auswahl', value: 'dropdown' },
],
},
// Symbole
{
name: 'symbols',
type: 'group',
label: 'Symbole',
fields: [
{
name: 'checkSymbol',
type: 'text',
defaultValue: '✓',
label: 'Häkchen-Symbol',
},
{
name: 'crossSymbol',
type: 'text',
defaultValue: '✗',
label: 'X-Symbol',
},
{
name: 'partialSymbol',
type: 'text',
defaultValue: '○',
label: 'Teilweise-Symbol',
},
],
},
// Styling
{
name: 'backgroundColor',
type: 'select',
defaultValue: 'white',
label: 'Hintergrund',
options: [
{ label: 'Weiß', value: 'white' },
{ label: 'Hell (Grau)', value: 'light' },
{ label: 'Dunkel', value: 'dark' },
],
},
{
name: 'tableStyle',
type: 'select',
defaultValue: 'bordered',
label: 'Tabellen-Stil',
options: [
{ label: 'Umrandet', value: 'bordered' },
{ label: 'Striped', value: 'striped' },
{ label: 'Minimalistisch', value: 'minimal' },
{ label: 'Karten', value: 'cards' },
],
admin: {
condition: (data, siblingData) =>
siblingData?.comparisonType === 'table' ||
siblingData?.comparisonType === 'feature-matrix',
},
},
{
name: 'checkColor',
type: 'select',
defaultValue: 'green',
label: 'Häkchen-Farbe',
options: [
{ label: 'Grün', value: 'green' },
{ label: 'Primär', value: 'primary' },
{ label: 'Blau', value: 'blue' },
],
},
{
name: 'crossColor',
type: 'select',
defaultValue: 'red',
label: 'X-Farbe',
options: [
{ label: 'Rot', value: 'red' },
{ label: 'Grau', value: 'gray' },
{ label: 'Transparent', value: 'transparent' },
],
},
],
}

View file

@ -0,0 +1,457 @@
import type { Block } from 'payload'
/**
* DownloadsBlock
*
* Zeigt Downloads mit Filter- und Suchfunktion.
* Unterstützt verschiedene Layouts und Kategoriefilter.
*/
export const DownloadsBlock: Block = {
slug: 'downloads-block',
labels: {
singular: 'Downloads',
plural: 'Downloads',
},
imageURL: '/assets/blocks/downloads.png',
fields: [
{
name: 'title',
type: 'text',
label: 'Überschrift',
localized: true,
defaultValue: 'Downloads',
},
{
name: 'subtitle',
type: 'textarea',
label: 'Untertitel',
localized: true,
},
{
name: 'introduction',
type: 'richText',
label: 'Einleitungstext',
localized: true,
},
// Datenquelle
{
name: 'source',
type: 'select',
defaultValue: 'all',
label: 'Downloads',
options: [
{ label: 'Alle aktiven', value: 'all' },
{ label: 'Nach Kategorie', value: 'category' },
{ label: 'Nach Tags', value: 'tags' },
{ label: 'Nur hervorgehobene', value: 'featured' },
{ label: 'Zugehörig zu Service', value: 'service' },
{ label: 'Zugehörig zu Produkt', value: 'product' },
{ label: 'Manuell auswählen', value: 'manual' },
],
},
{
name: 'category',
type: 'select',
label: 'Kategorie',
options: [
{ label: 'Broschüre', value: 'brochure' },
{ label: 'Flyer', value: 'flyer' },
{ label: 'Katalog', value: 'catalog' },
{ label: 'Preisliste', value: 'pricelist' },
{ label: 'Formular', value: 'form' },
{ label: 'Anleitung', value: 'manual' },
{ label: 'Datenblatt', value: 'datasheet' },
{ label: 'Zertifikat', value: 'certificate' },
{ label: 'Pressematerial', value: 'press' },
{ label: 'Whitepaper', value: 'whitepaper' },
{ label: 'Präsentation', value: 'presentation' },
{ label: 'Vertrag/AGB', value: 'legal' },
],
admin: {
condition: (_, siblingData) => siblingData?.source === 'category',
},
},
{
name: 'filterTags',
type: 'text',
label: 'Tags (kommagetrennt)',
admin: {
condition: (_, siblingData) => siblingData?.source === 'tags',
description: 'z.B. "produktinfo, 2024, neu"',
},
},
{
name: 'relatedService',
type: 'relationship',
relationTo: 'services',
label: 'Zugehöriger Service',
admin: {
condition: (_, siblingData) => siblingData?.source === 'service',
},
},
{
name: 'relatedProduct',
type: 'relationship',
relationTo: 'products',
label: 'Zugehöriges Produkt',
admin: {
condition: (_, siblingData) => siblingData?.source === 'product',
},
},
{
name: 'selectedDownloads',
type: 'relationship',
relationTo: 'downloads',
hasMany: true,
label: 'Downloads auswählen',
admin: {
condition: (_, siblingData) => siblingData?.source === 'manual',
},
},
{
name: 'limit',
type: 'number',
defaultValue: 20,
min: 1,
max: 100,
label: 'Maximale Anzahl',
},
// Filter-UI
{
name: 'filters',
type: 'group',
label: 'Filter-Optionen',
fields: [
{
name: 'showSearch',
type: 'checkbox',
defaultValue: true,
label: 'Suchfeld',
},
{
name: 'showCategoryFilter',
type: 'checkbox',
defaultValue: true,
label: 'Kategorie-Filter',
},
{
name: 'showFileTypeFilter',
type: 'checkbox',
defaultValue: false,
label: 'Dateityp-Filter',
},
{
name: 'filterLayout',
type: 'select',
defaultValue: 'horizontal',
label: 'Filter-Layout',
options: [
{ label: 'Horizontal', value: 'horizontal' },
{ label: 'Sidebar', value: 'sidebar' },
{ label: 'Dropdown', value: 'dropdown' },
],
},
],
},
// Layout
{
name: 'layout',
type: 'select',
defaultValue: 'list',
label: 'Layout',
options: [
{ label: 'Liste', value: 'list' },
{ label: 'Karten', value: 'cards' },
{ label: 'Kompakt', value: 'compact' },
{ label: 'Tabelle', value: 'table' },
{ label: 'Akkordeon (nach Kategorie)', value: 'accordion' },
],
},
{
name: 'columns',
type: 'select',
defaultValue: '1',
label: 'Spalten',
options: [
{ label: '1 Spalte', value: '1' },
{ label: '2 Spalten', value: '2' },
{ label: '3 Spalten', value: '3' },
{ label: '4 Spalten', value: '4' },
],
admin: {
condition: (_, siblingData) =>
siblingData?.layout === 'cards' || siblingData?.layout === 'list',
},
},
// Anzuzeigende Informationen
{
name: 'show',
type: 'group',
label: 'Anzeigen',
fields: [
{
name: 'thumbnail',
type: 'checkbox',
defaultValue: true,
label: 'Vorschaubild/Icon',
},
{
name: 'description',
type: 'checkbox',
defaultValue: true,
label: 'Beschreibung',
},
{
name: 'fileSize',
type: 'checkbox',
defaultValue: true,
label: 'Dateigröße',
},
{
name: 'fileType',
type: 'checkbox',
defaultValue: true,
label: 'Dateityp',
},
{
name: 'category',
type: 'checkbox',
defaultValue: true,
label: 'Kategorie',
},
{
name: 'downloadCount',
type: 'checkbox',
defaultValue: false,
label: 'Download-Zähler',
},
{
name: 'version',
type: 'checkbox',
defaultValue: false,
label: 'Version',
},
{
name: 'lastUpdated',
type: 'checkbox',
defaultValue: false,
label: 'Letztes Update',
},
],
},
// Download-Verhalten
{
name: 'downloadBehavior',
type: 'group',
label: 'Download-Verhalten',
fields: [
{
name: 'directDownload',
type: 'checkbox',
defaultValue: true,
label: 'Direkter Download',
admin: {
description: 'Datei direkt herunterladen vs. Vorschau öffnen',
},
},
{
name: 'trackDownloads',
type: 'checkbox',
defaultValue: true,
label: 'Downloads zählen',
},
{
name: 'openInNewTab',
type: 'checkbox',
defaultValue: false,
label: 'In neuem Tab öffnen',
},
],
},
// File-Type Icons
{
name: 'fileIcons',
type: 'group',
label: 'Datei-Icons',
fields: [
{
name: 'showFileTypeIcon',
type: 'checkbox',
defaultValue: true,
label: 'Dateityp-Icon anzeigen',
},
{
name: 'iconStyle',
type: 'select',
defaultValue: 'colored',
label: 'Icon-Stil',
options: [
{ label: 'Farbig', value: 'colored' },
{ label: 'Monochrom', value: 'mono' },
{ label: 'Outline', value: 'outline' },
],
admin: {
condition: (_, siblingData) => siblingData?.showFileTypeIcon,
},
},
],
},
// Sortierung
{
name: 'sortBy',
type: 'select',
defaultValue: 'order',
label: 'Sortierung',
options: [
{ label: 'Manuelle Reihenfolge', value: 'order' },
{ label: 'Titel (A-Z)', value: 'title_asc' },
{ label: 'Titel (Z-A)', value: 'title_desc' },
{ label: 'Neueste zuerst', value: 'date_desc' },
{ label: 'Älteste zuerst', value: 'date_asc' },
{ label: 'Beliebteste', value: 'downloads_desc' },
{ label: 'Dateigröße', value: 'size' },
],
},
// Gruppierung
{
name: 'groupBy',
type: 'select',
defaultValue: 'none',
label: 'Gruppieren nach',
options: [
{ label: 'Keine Gruppierung', value: 'none' },
{ label: 'Kategorie', value: 'category' },
{ label: 'Dateityp', value: 'fileType' },
],
},
// Pagination
{
name: 'pagination',
type: 'group',
label: 'Pagination',
fields: [
{
name: 'type',
type: 'select',
defaultValue: 'none',
label: 'Typ',
options: [
{ label: 'Alle anzeigen', value: 'none' },
{ label: '"Mehr laden" Button', value: 'button' },
{ label: 'Pagination', value: 'pagination' },
],
},
{
name: 'perPage',
type: 'number',
defaultValue: 10,
label: 'Pro Seite',
admin: {
condition: (_, siblingData) =>
siblingData?.type === 'pagination' || siblingData?.type === 'button',
},
},
],
},
// Styling
{
name: 'style',
type: 'group',
label: 'Darstellung',
fields: [
{
name: 'bg',
type: 'select',
defaultValue: 'none',
label: 'Hintergrund',
options: [
{ label: 'Keiner', value: 'none' },
{ label: 'Hell', value: 'light' },
{ label: 'Dunkel', value: 'dark' },
],
},
{
name: 'cardStyle',
type: 'select',
defaultValue: 'bordered',
label: 'Karten-Stil',
options: [
{ label: 'Einfach', value: 'simple' },
{ label: 'Mit Rahmen', value: 'bordered' },
{ label: 'Mit Schatten', value: 'shadow' },
],
},
{
name: 'hoverEffect',
type: 'select',
defaultValue: 'highlight',
label: 'Hover-Effekt',
options: [
{ label: 'Keiner', value: 'none' },
{ label: 'Hervorheben', value: 'highlight' },
{ label: 'Anheben', value: 'lift' },
],
},
{
name: 'gap',
type: 'select',
defaultValue: '16',
label: 'Abstand',
options: [
{ label: 'Klein (8px)', value: '8' },
{ label: 'Normal (16px)', value: '16' },
{ label: 'Groß (24px)', value: '24' },
],
},
],
},
// Empty State
{
name: 'emptyState',
type: 'group',
label: 'Keine Downloads',
fields: [
{
name: 'title',
type: 'text',
label: 'Überschrift',
localized: true,
defaultValue: 'Keine Downloads verfügbar',
},
{
name: 'message',
type: 'textarea',
label: 'Nachricht',
localized: true,
defaultValue: 'Aktuell sind keine Downloads in dieser Kategorie verfügbar.',
},
],
},
// CTA
{
name: 'showAllDownloadsLink',
type: 'checkbox',
defaultValue: false,
label: '"Alle Downloads" Link',
},
{
name: 'allDownloadsText',
type: 'text',
label: 'Link-Text',
localized: true,
defaultValue: 'Alle Downloads ansehen',
admin: {
condition: (_, siblingData) => siblingData?.showAllDownloadsLink,
},
},
{
name: 'allDownloadsUrl',
type: 'text',
label: 'Link-URL',
defaultValue: '/downloads',
admin: {
condition: (_, siblingData) => siblingData?.showAllDownloadsLink,
},
},
],
}

320
src/blocks/EventsBlock.ts Normal file
View file

@ -0,0 +1,320 @@
import type { Block } from 'payload'
/**
* Events Block
*
* Zeigt Events aus der Events Collection oder inline-definierte Events.
* Unterstützt verschiedene Layouts (Kalender, Liste, Karten, Timeline)
* und Filter nach Event-Typ, Zeitraum oder Kategorie.
*/
export const EventsBlock: Block = {
slug: 'events',
labels: {
singular: 'Events',
plural: 'Events',
},
fields: [
{
name: 'title',
type: 'text',
label: 'Überschrift',
localized: true,
},
{
name: 'subtitle',
type: 'text',
label: 'Untertitel',
localized: true,
},
// Quelle
{
name: 'sourceMode',
type: 'select',
defaultValue: 'collection',
label: 'Quelle',
options: [
{ label: 'Aus Events Collection', value: 'collection' },
{ label: 'Handverlesene Auswahl', value: 'selected' },
],
},
// Collection Filter
{
name: 'filterMode',
type: 'select',
defaultValue: 'upcoming',
label: 'Filter',
options: [
{ label: 'Kommende Events', value: 'upcoming' },
{ label: 'Vergangene Events', value: 'past' },
{ label: 'Alle Events', value: 'all' },
{ label: 'Nur hervorgehobene', value: 'featured' },
{ label: 'Nach Typ', value: 'type' },
{ label: 'Nach Kategorie', value: 'category' },
],
admin: {
condition: (data, siblingData) => siblingData?.sourceMode === 'collection',
},
},
{
name: 'eventType',
type: 'select',
label: 'Event-Typ filtern',
options: [
{ label: 'Veranstaltung', value: 'event' },
{ label: 'Workshop', value: 'workshop' },
{ label: 'Seminar', value: 'seminar' },
{ label: 'Webinar', value: 'webinar' },
{ label: 'Konferenz', value: 'conference' },
{ label: 'Messe', value: 'tradeshow' },
{ label: 'Networking', value: 'networking' },
{ label: 'Kurs', value: 'course' },
{ label: 'Vortrag', value: 'talk' },
],
admin: {
condition: (data, siblingData) =>
siblingData?.sourceMode === 'collection' && siblingData?.filterMode === 'type',
},
},
{
name: 'category',
type: 'text',
label: 'Kategorie filtern',
admin: {
condition: (data, siblingData) =>
siblingData?.sourceMode === 'collection' && siblingData?.filterMode === 'category',
},
},
{
name: 'selectedEvents',
type: 'relationship',
relationTo: 'events' as 'users',
hasMany: true,
label: 'Events auswählen',
admin: {
condition: (data, siblingData) => siblingData?.sourceMode === 'selected',
},
},
{
name: 'limit',
type: 'number',
defaultValue: 6,
min: 1,
max: 50,
label: 'Maximale Anzahl',
admin: {
condition: (data, siblingData) => siblingData?.sourceMode === 'collection',
},
},
// Layout
{
name: 'layout',
type: 'select',
defaultValue: 'cards',
label: 'Layout',
options: [
{ label: 'Karten (Grid)', value: 'cards' },
{ label: 'Liste', value: 'list' },
{ label: 'Kompakte Liste', value: 'compact' },
{ label: 'Timeline', value: 'timeline' },
{ label: 'Kalender', value: 'calendar' },
{ label: 'Agenda', value: 'agenda' },
],
},
{
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) => siblingData?.layout === 'cards',
},
},
// Anzeigeoptionen
{
name: 'showImage',
type: 'checkbox',
defaultValue: true,
label: 'Bild anzeigen',
},
{
name: 'showExcerpt',
type: 'checkbox',
defaultValue: true,
label: 'Beschreibung anzeigen',
},
{
name: 'showDate',
type: 'checkbox',
defaultValue: true,
label: 'Datum anzeigen',
},
{
name: 'showTime',
type: 'checkbox',
defaultValue: true,
label: 'Uhrzeit anzeigen',
},
{
name: 'showLocation',
type: 'checkbox',
defaultValue: true,
label: 'Ort anzeigen',
},
{
name: 'showPrice',
type: 'checkbox',
defaultValue: false,
label: 'Preis anzeigen',
},
{
name: 'showEventType',
type: 'checkbox',
defaultValue: false,
label: 'Event-Typ Badge anzeigen',
},
{
name: 'showRegistrationButton',
type: 'checkbox',
defaultValue: true,
label: 'Anmelde-Button anzeigen',
},
{
name: 'registrationButtonText',
type: 'text',
defaultValue: 'Jetzt anmelden',
label: 'Button-Text',
localized: true,
admin: {
condition: (data, siblingData) => siblingData?.showRegistrationButton,
},
},
// Kalender-Optionen
{
name: 'calendarOptions',
type: 'group',
label: 'Kalender-Optionen',
admin: {
condition: (data, siblingData) => siblingData?.layout === 'calendar',
},
fields: [
{
name: 'defaultView',
type: 'select',
defaultValue: 'month',
label: 'Standard-Ansicht',
options: [
{ label: 'Monat', value: 'month' },
{ label: 'Woche', value: 'week' },
{ label: 'Tag', value: 'day' },
],
},
{
name: 'showNavigation',
type: 'checkbox',
defaultValue: true,
label: 'Navigation anzeigen',
},
{
name: 'showViewToggle',
type: 'checkbox',
defaultValue: false,
label: 'Ansichts-Wechsel anzeigen',
},
],
},
// Gruppierung
{
name: 'groupBy',
type: 'select',
defaultValue: 'none',
label: 'Gruppieren nach',
options: [
{ label: 'Keine Gruppierung', value: 'none' },
{ label: 'Monat', value: 'month' },
{ label: 'Event-Typ', value: 'type' },
{ label: 'Kategorie', value: 'category' },
],
admin: {
condition: (data, siblingData) =>
siblingData?.layout === 'list' || siblingData?.layout === 'compact',
},
},
// Sortierung
{
name: 'sortOrder',
type: 'select',
defaultValue: 'asc',
label: 'Sortierung',
options: [
{ label: 'Aufsteigend (älteste zuerst)', value: 'asc' },
{ label: 'Absteigend (neueste zuerst)', value: 'desc' },
],
},
// CTA
{
name: 'showAllLink',
type: 'checkbox',
defaultValue: true,
label: '"Alle Events" Link anzeigen',
},
{
name: 'allEventsLink',
type: 'text',
defaultValue: '/events',
label: 'Link zu allen Events',
admin: {
condition: (data, siblingData) => siblingData?.showAllLink,
},
},
{
name: 'allEventsText',
type: 'text',
defaultValue: 'Alle Veranstaltungen',
label: 'Link-Text',
localized: true,
admin: {
condition: (data, siblingData) => siblingData?.showAllLink,
},
},
// Leerer Zustand
{
name: 'emptyMessage',
type: 'text',
defaultValue: 'Aktuell sind keine Veranstaltungen geplant.',
label: 'Nachricht wenn keine Events',
localized: true,
},
// Styling
{
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' },
],
},
{
name: 'cardStyle',
type: 'select',
defaultValue: 'elevated',
label: 'Karten-Stil',
options: [
{ label: 'Erhöht (Schatten)', value: 'elevated' },
{ label: 'Umrandet', value: 'bordered' },
{ label: 'Flach', value: 'flat' },
],
admin: {
condition: (data, siblingData) => siblingData?.layout === 'cards',
},
},
],
}

436
src/blocks/JobsBlock.ts Normal file
View file

@ -0,0 +1,436 @@
import type { Block } from 'payload'
/**
* JobsBlock
*
* Zeigt Stellenanzeigen mit Filter- und Suchfunktion.
* Unterstützt verschiedene Layouts und Filteroptionen.
*/
export const JobsBlock: Block = {
slug: 'jobs-block',
labels: {
singular: 'Stellenanzeigen',
plural: 'Stellenanzeigen',
},
imageURL: '/assets/blocks/jobs.png',
fields: [
{
name: 'title',
type: 'text',
label: 'Überschrift',
localized: true,
defaultValue: 'Offene Stellen',
},
{
name: 'subtitle',
type: 'textarea',
label: 'Untertitel',
localized: true,
},
{
name: 'introduction',
type: 'richText',
label: 'Einleitungstext',
localized: true,
},
// Datenquelle
{
name: 'source',
type: 'select',
defaultValue: 'all',
label: 'Stellen',
options: [
{ label: 'Alle veröffentlichten', value: 'all' },
{ label: 'Nach Kategorie', value: 'category' },
{ label: 'Nach Abteilung', value: 'department' },
{ label: 'Nach Standort', value: 'location' },
{ label: 'Nur hervorgehobene', value: 'featured' },
{ label: 'Manuell auswählen', value: 'manual' },
],
},
{
name: 'category',
type: 'select',
label: 'Kategorie',
options: [
{ label: 'Pflege & Betreuung', value: 'care' },
{ label: 'Medizin', value: 'medical' },
{ label: 'Verwaltung', value: 'admin' },
{ label: 'IT & Technik', value: 'it' },
{ label: 'Marketing', value: 'marketing' },
{ label: 'Vertrieb', value: 'sales' },
{ label: 'Finanzen', value: 'finance' },
{ label: 'Personal', value: 'hr' },
{ label: 'Produktion', value: 'production' },
{ label: 'Logistik', value: 'logistics' },
],
admin: {
condition: (_, siblingData) => siblingData?.source === 'category',
},
},
{
name: 'department',
type: 'text',
label: 'Abteilung',
admin: {
condition: (_, siblingData) => siblingData?.source === 'department',
},
},
{
name: 'locationFilter',
type: 'relationship',
relationTo: 'locations',
label: 'Standort',
admin: {
condition: (_, siblingData) => siblingData?.source === 'location',
},
},
{
name: 'selectedJobs',
type: 'relationship',
relationTo: 'jobs',
hasMany: true,
label: 'Stellen auswählen',
admin: {
condition: (_, siblingData) => siblingData?.source === 'manual',
},
},
{
name: 'limit',
type: 'number',
defaultValue: 10,
min: 1,
max: 50,
label: 'Maximale Anzahl',
},
// Filter-UI
{
name: 'filters',
type: 'group',
label: 'Filter-Optionen',
fields: [
{
name: 'showSearch',
type: 'checkbox',
defaultValue: true,
label: 'Suchfeld',
},
{
name: 'showCategoryFilter',
type: 'checkbox',
defaultValue: true,
label: 'Kategorie-Filter',
},
{
name: 'showTypeFilter',
type: 'checkbox',
defaultValue: true,
label: 'Beschäftigungsart-Filter',
},
{
name: 'showLocationFilter',
type: 'checkbox',
defaultValue: true,
label: 'Standort-Filter',
},
{
name: 'showWorkModelFilter',
type: 'checkbox',
defaultValue: false,
label: 'Arbeitsmodell-Filter',
},
{
name: 'filterLayout',
type: 'select',
defaultValue: 'horizontal',
label: 'Filter-Layout',
options: [
{ label: 'Horizontal', value: 'horizontal' },
{ label: 'Sidebar', value: 'sidebar' },
{ label: 'Dropdown', value: 'dropdown' },
],
},
],
},
// Layout
{
name: 'layout',
type: 'select',
defaultValue: 'list',
label: 'Layout',
options: [
{ label: 'Liste', value: 'list' },
{ label: 'Karten', value: 'cards' },
{ label: 'Kompakt', value: 'compact' },
{ label: 'Akkordeon', value: 'accordion' },
],
},
{
name: 'columns',
type: 'select',
defaultValue: '1',
label: 'Spalten',
options: [
{ label: '1 Spalte', value: '1' },
{ label: '2 Spalten', value: '2' },
{ label: '3 Spalten', value: '3' },
],
admin: {
condition: (_, siblingData) =>
siblingData?.layout === 'cards' || siblingData?.layout === 'list',
},
},
// Anzuzeigende Informationen
{
name: 'show',
type: 'group',
label: 'Anzeigen',
fields: [
{
name: 'image',
type: 'checkbox',
defaultValue: false,
label: 'Bild',
},
{
name: 'department',
type: 'checkbox',
defaultValue: true,
label: 'Abteilung',
},
{
name: 'type',
type: 'checkbox',
defaultValue: true,
label: 'Beschäftigungsart',
},
{
name: 'location',
type: 'checkbox',
defaultValue: true,
label: 'Standort',
},
{
name: 'workModel',
type: 'checkbox',
defaultValue: true,
label: 'Arbeitsmodell',
},
{
name: 'salary',
type: 'checkbox',
defaultValue: false,
label: 'Gehalt',
},
{
name: 'summary',
type: 'checkbox',
defaultValue: true,
label: 'Kurzbeschreibung',
},
{
name: 'deadline',
type: 'checkbox',
defaultValue: false,
label: 'Bewerbungsfrist',
},
{
name: 'publishDate',
type: 'checkbox',
defaultValue: true,
label: 'Veröffentlichungsdatum',
},
{
name: 'badges',
type: 'checkbox',
defaultValue: true,
label: 'Badges (Neu, Dringend)',
},
],
},
// Badges
{
name: 'badges',
type: 'group',
label: 'Badge-Einstellungen',
fields: [
{
name: 'newDays',
type: 'number',
defaultValue: 7,
label: '"Neu" für X Tage',
admin: {
description: 'Wie viele Tage nach Veröffentlichung "Neu" anzeigen',
},
},
{
name: 'showUrgent',
type: 'checkbox',
defaultValue: true,
label: '"Dringend" Badge',
},
{
name: 'showFeatured',
type: 'checkbox',
defaultValue: true,
label: '"Top" Badge für hervorgehobene',
},
],
},
// Pagination
{
name: 'pagination',
type: 'group',
label: 'Pagination',
fields: [
{
name: 'type',
type: 'select',
defaultValue: 'button',
label: 'Typ',
options: [
{ label: '"Mehr laden" Button', value: 'button' },
{ label: 'Pagination', value: 'pagination' },
{ label: 'Infinite Scroll', value: 'infinite' },
{ label: 'Alle anzeigen', value: 'none' },
],
},
{
name: 'perPage',
type: 'number',
defaultValue: 10,
label: 'Pro Seite',
admin: {
condition: (_, siblingData) =>
siblingData?.type === 'pagination' || siblingData?.type === 'button',
},
},
],
},
// Styling
{
name: 'style',
type: 'group',
label: 'Darstellung',
fields: [
{
name: 'bg',
type: 'select',
defaultValue: 'none',
label: 'Hintergrund',
options: [
{ label: 'Keiner', value: 'none' },
{ label: 'Hell', value: 'light' },
{ label: 'Dunkel', value: 'dark' },
],
},
{
name: 'cardStyle',
type: 'select',
defaultValue: 'bordered',
label: 'Karten-Stil',
options: [
{ label: 'Einfach', value: 'simple' },
{ label: 'Mit Rahmen', value: 'bordered' },
{ label: 'Mit Schatten', value: 'shadow' },
],
},
{
name: 'hoverEffect',
type: 'select',
defaultValue: 'lift',
label: 'Hover-Effekt',
options: [
{ label: 'Keiner', value: 'none' },
{ label: 'Anheben', value: 'lift' },
{ label: 'Hervorheben', value: 'highlight' },
],
},
{
name: 'gap',
type: 'select',
defaultValue: '16',
label: 'Abstand',
options: [
{ label: 'Klein (8px)', value: '8' },
{ label: 'Normal (16px)', value: '16' },
{ label: 'Groß (24px)', value: '24' },
],
},
],
},
// Empty State
{
name: 'emptyState',
type: 'group',
label: 'Keine Stellen',
fields: [
{
name: 'title',
type: 'text',
label: 'Überschrift',
localized: true,
defaultValue: 'Aktuell keine offenen Stellen',
},
{
name: 'message',
type: 'textarea',
label: 'Nachricht',
localized: true,
defaultValue: 'Schauen Sie später wieder vorbei oder senden Sie uns eine Initiativbewerbung.',
},
{
name: 'showInitiativeLink',
type: 'checkbox',
defaultValue: true,
label: 'Initiativbewerbungs-Link',
},
{
name: 'initiativeText',
type: 'text',
label: 'Link-Text',
localized: true,
defaultValue: 'Initiativbewerbung senden',
admin: {
condition: (_, siblingData) => siblingData?.showInitiativeLink,
},
},
{
name: 'initiativeUrl',
type: 'text',
label: 'Link-URL',
defaultValue: '/karriere/initiativbewerbung',
admin: {
condition: (_, siblingData) => siblingData?.showInitiativeLink,
},
},
],
},
// CTA
{
name: 'showAllJobsLink',
type: 'checkbox',
defaultValue: false,
label: '"Alle Stellen" Link',
},
{
name: 'allJobsText',
type: 'text',
label: 'Link-Text',
localized: true,
defaultValue: 'Alle offenen Stellen',
admin: {
condition: (_, siblingData) => siblingData?.showAllJobsLink,
},
},
{
name: 'allJobsUrl',
type: 'text',
label: 'Link-URL',
defaultValue: '/karriere',
admin: {
condition: (_, siblingData) => siblingData?.showAllJobsLink,
},
},
],
}

View file

@ -0,0 +1,386 @@
import type { Block } from 'payload'
/**
* LocationsBlock
*
* Zeigt Firmenstandorte mit Karte, Adresse, Öffnungszeiten und Kontaktdaten.
* Unterstützt verschiedene Layouts und Kartenanbieter.
*/
export const LocationsBlock: Block = {
slug: 'locations-block',
labels: {
singular: 'Standorte',
plural: 'Standorte',
},
imageURL: '/assets/blocks/locations.png',
fields: [
{
name: 'title',
type: 'text',
label: 'Überschrift',
localized: true,
defaultValue: 'Unsere Standorte',
},
{
name: 'subtitle',
type: 'textarea',
label: 'Untertitel',
localized: true,
},
// Datenquelle
{
name: 'source',
type: 'select',
defaultValue: 'all',
label: 'Standorte',
options: [
{ label: 'Alle aktiven Standorte', value: 'all' },
{ label: 'Nur Hauptstandort', value: 'main' },
{ label: 'Nach Typ', value: 'type' },
{ label: 'Manuell auswählen', value: 'manual' },
],
},
{
name: 'locationType',
type: 'select',
label: 'Standort-Typ',
options: [
{ label: 'Hauptsitz', value: 'headquarters' },
{ label: 'Büro', value: 'office' },
{ label: 'Filiale', value: 'branch' },
{ label: 'Lager', value: 'warehouse' },
{ label: 'Showroom', value: 'showroom' },
{ label: 'Studio', value: 'studio' },
{ label: 'Praxis', value: 'practice' },
{ label: 'Werkstatt', value: 'workshop' },
],
admin: {
condition: (_, siblingData) => siblingData?.source === 'type',
},
},
{
name: 'selectedLocations',
type: 'relationship',
relationTo: 'locations',
hasMany: true,
label: 'Standorte auswählen',
admin: {
condition: (_, siblingData) => siblingData?.source === 'manual',
},
},
// Layout
{
name: 'layout',
type: 'select',
defaultValue: 'map-list',
label: 'Layout',
options: [
{ label: 'Karte + Liste', value: 'map-list' },
{ label: 'Nur Karte', value: 'map-only' },
{ label: 'Nur Liste', value: 'list-only' },
{ label: 'Karten-Grid', value: 'cards' },
{ label: 'Akkordeon', value: 'accordion' },
{ label: 'Tabs', value: 'tabs' },
],
},
{
name: 'mapPosition',
type: 'select',
defaultValue: 'left',
label: 'Karten-Position',
options: [
{ label: 'Links', value: 'left' },
{ label: 'Rechts', value: 'right' },
{ label: 'Oben', value: 'top' },
{ label: 'Unten', value: 'bottom' },
],
admin: {
condition: (_, siblingData) => siblingData?.layout === 'map-list',
},
},
{
name: 'columns',
type: 'select',
defaultValue: '2',
label: 'Spalten',
options: [
{ label: '1 Spalte', value: '1' },
{ label: '2 Spalten', value: '2' },
{ label: '3 Spalten', value: '3' },
],
admin: {
condition: (_, siblingData) => siblingData?.layout === 'cards',
},
},
// Karten-Einstellungen
{
name: 'map',
type: 'group',
label: 'Karten-Einstellungen',
admin: {
condition: (_, siblingData) =>
siblingData?.layout === 'map-list' || siblingData?.layout === 'map-only',
},
fields: [
{
name: 'provider',
type: 'select',
defaultValue: 'osm',
label: 'Kartenanbieter',
options: [
{ label: 'OpenStreetMap', value: 'osm' },
{ label: 'Google Maps', value: 'google' },
{ label: 'Mapbox', value: 'mapbox' },
],
},
{
name: 'style',
type: 'select',
defaultValue: 'default',
label: 'Kartenstil',
options: [
{ label: 'Standard', value: 'default' },
{ label: 'Hell', value: 'light' },
{ label: 'Dunkel', value: 'dark' },
{ label: 'Satellite', value: 'satellite' },
],
},
{
name: 'height',
type: 'select',
defaultValue: '400',
label: 'Höhe',
options: [
{ label: 'Klein (300px)', value: '300' },
{ label: 'Mittel (400px)', value: '400' },
{ label: 'Groß (500px)', value: '500' },
{ label: 'Sehr groß (600px)', value: '600' },
],
},
{
name: 'defaultZoom',
type: 'number',
defaultValue: 12,
min: 1,
max: 20,
label: 'Standard-Zoom',
},
{
name: 'fitBounds',
type: 'checkbox',
defaultValue: true,
label: 'Alle Marker zeigen',
admin: {
description: 'Karte automatisch anpassen, um alle Standorte zu zeigen',
},
},
{
name: 'markerStyle',
type: 'select',
defaultValue: 'pin',
label: 'Marker-Stil',
options: [
{ label: 'Pin', value: 'pin' },
{ label: 'Punkt', value: 'dot' },
{ label: 'Icon', value: 'icon' },
{ label: 'Custom', value: 'custom' },
],
},
{
name: 'markerColor',
type: 'text',
label: 'Marker-Farbe',
defaultValue: '#e11d48',
admin: {
description: 'Hex-Farbe (z.B. #e11d48)',
},
},
{
name: 'clustering',
type: 'checkbox',
defaultValue: true,
label: 'Marker clustern',
admin: {
description: 'Nahe Marker zu Gruppen zusammenfassen',
},
},
{
name: 'controls',
type: 'group',
label: 'Steuerung',
fields: [
{
name: 'zoom',
type: 'checkbox',
defaultValue: true,
label: 'Zoom-Buttons',
},
{
name: 'fullscreen',
type: 'checkbox',
defaultValue: true,
label: 'Vollbild-Button',
},
{
name: 'scrollZoom',
type: 'checkbox',
defaultValue: false,
label: 'Scroll-Zoom',
},
{
name: 'dragging',
type: 'checkbox',
defaultValue: true,
label: 'Verschiebbar',
},
],
},
],
},
// Anzuzeigende Informationen
{
name: 'show',
type: 'group',
label: 'Anzeigen',
fields: [
{
name: 'image',
type: 'checkbox',
defaultValue: true,
label: 'Bild',
},
{
name: 'type',
type: 'checkbox',
defaultValue: false,
label: 'Standort-Typ',
},
{
name: 'address',
type: 'checkbox',
defaultValue: true,
label: 'Adresse',
},
{
name: 'phone',
type: 'checkbox',
defaultValue: true,
label: 'Telefon',
},
{
name: 'email',
type: 'checkbox',
defaultValue: true,
label: 'E-Mail',
},
{
name: 'hours',
type: 'checkbox',
defaultValue: true,
label: 'Öffnungszeiten',
},
{
name: 'directions',
type: 'checkbox',
defaultValue: false,
label: 'Anfahrt',
},
{
name: 'services',
type: 'checkbox',
defaultValue: false,
label: 'Services',
},
{
name: 'team',
type: 'checkbox',
defaultValue: false,
label: 'Team-Mitglieder',
},
],
},
// Aktionen
{
name: 'actions',
type: 'group',
label: 'Aktionen',
fields: [
{
name: 'showDirectionsLink',
type: 'checkbox',
defaultValue: true,
label: '"Route planen" Link',
},
{
name: 'showCallButton',
type: 'checkbox',
defaultValue: true,
label: 'Anruf-Button (mobil)',
},
{
name: 'showEmailButton',
type: 'checkbox',
defaultValue: false,
label: 'E-Mail-Button',
},
{
name: 'showDetailLink',
type: 'checkbox',
defaultValue: false,
label: 'Link zur Detail-Seite',
},
{
name: 'detailBasePath',
type: 'text',
label: 'Detail-Basis-Pfad',
defaultValue: '/standorte',
admin: {
condition: (_, siblingData) => siblingData?.showDetailLink,
},
},
],
},
// Styling
{
name: 'style',
type: 'group',
label: 'Darstellung',
fields: [
{
name: 'bg',
type: 'select',
defaultValue: 'none',
label: 'Hintergrund',
options: [
{ label: 'Keiner', value: 'none' },
{ label: 'Hell', value: 'light' },
{ label: 'Dunkel', value: 'dark' },
],
},
{
name: 'cardStyle',
type: 'select',
defaultValue: 'bordered',
label: 'Karten-Stil',
options: [
{ label: 'Einfach', value: 'simple' },
{ label: 'Mit Rahmen', value: 'bordered' },
{ label: 'Mit Schatten', value: 'shadow' },
{ label: 'Gefüllt', value: 'filled' },
],
},
{
name: 'gap',
type: 'select',
defaultValue: '24',
label: 'Abstand',
options: [
{ label: 'Klein (16px)', value: '16' },
{ label: 'Normal (24px)', value: '24' },
{ label: 'Groß (32px)', value: '32' },
],
},
],
},
],
}

422
src/blocks/LogoGridBlock.ts Normal file
View file

@ -0,0 +1,422 @@
import type { Block } from 'payload'
/**
* LogoGridBlock
*
* Zeigt Partner-, Kunden- oder Zertifizierungs-Logos in verschiedenen Layouts.
* Kann als Slider, Grid oder Marquee dargestellt werden.
*/
export const LogoGridBlock: Block = {
slug: 'logo-grid-block',
labels: {
singular: 'Logo-Grid',
plural: 'Logo-Grids',
},
imageURL: '/assets/blocks/logo-grid.png',
fields: [
{
name: 'title',
type: 'text',
label: 'Überschrift',
localized: true,
admin: {
description: 'z.B. "Unsere Partner", "Bekannt aus", "Zertifizierungen"',
},
},
{
name: 'subtitle',
type: 'textarea',
label: 'Untertitel',
localized: true,
},
// Datenquelle
{
name: 'source',
type: 'select',
defaultValue: 'collection',
label: 'Datenquelle',
options: [
{ label: 'Aus Partner-Collection', value: 'collection' },
{ label: 'Manuell hochladen', value: 'manual' },
],
},
// Collection-basiert
{
name: 'partnerType',
type: 'select',
label: 'Partner-Typ',
hasMany: true,
options: [
{ label: 'Alle', value: 'all' },
{ label: 'Partner', value: 'partner' },
{ label: 'Kunden', value: 'client' },
{ label: 'Referenzen', value: 'reference' },
{ label: 'Sponsoren', value: 'sponsor' },
{ label: 'Zertifizierungen', value: 'certification' },
{ label: 'Mitgliedschaften', value: 'membership' },
{ label: 'Auszeichnungen', value: 'award' },
{ label: 'Lieferanten', value: 'supplier' },
{ label: 'Technologien', value: 'technology' },
],
defaultValue: ['all'],
admin: {
condition: (_, siblingData) => siblingData?.source === 'collection',
},
},
{
name: 'selectedPartners',
type: 'relationship',
relationTo: 'partners',
hasMany: true,
label: 'Partner auswählen',
admin: {
condition: (_, siblingData) => siblingData?.source === 'collection',
description: 'Optional: Bestimmte Partner auswählen (sonst alle vom gewählten Typ)',
},
},
{
name: 'featuredOnly',
type: 'checkbox',
defaultValue: false,
label: 'Nur hervorgehobene',
admin: {
condition: (_, siblingData) => siblingData?.source === 'collection',
},
},
{
name: 'limit',
type: 'number',
defaultValue: 12,
min: 1,
max: 50,
label: 'Maximale Anzahl',
admin: {
condition: (_, siblingData) => siblingData?.source === 'collection',
},
},
// Manuell
{
name: 'logos',
type: 'array',
label: 'Logos',
admin: {
condition: (_, siblingData) => siblingData?.source === 'manual',
},
fields: [
{
name: 'logo',
type: 'upload',
relationTo: 'media',
required: true,
label: 'Logo',
},
{
name: 'name',
type: 'text',
label: 'Name',
admin: {
description: 'Für Alt-Text und Tooltip',
},
},
{
name: 'link',
type: 'text',
label: 'Link',
},
],
},
// Layout
{
name: 'layout',
type: 'select',
defaultValue: 'grid',
label: 'Layout',
options: [
{ label: 'Grid', value: 'grid' },
{ label: 'Slider', value: 'slider' },
{ label: 'Marquee (endlos)', value: 'marquee' },
{ label: 'Inline (flexibel)', value: 'inline' },
{ label: 'Masonry', value: 'masonry' },
],
},
{
name: 'columns',
type: 'select',
defaultValue: '4',
label: 'Spalten',
options: [
{ label: '3 Spalten', value: '3' },
{ label: '4 Spalten', value: '4' },
{ label: '5 Spalten', value: '5' },
{ label: '6 Spalten', value: '6' },
{ label: '8 Spalten', value: '8' },
],
admin: {
condition: (_, siblingData) => siblingData?.layout === 'grid',
},
},
// Slider-Optionen
{
name: 'slider',
type: 'group',
label: 'Slider-Einstellungen',
admin: {
condition: (_, siblingData) =>
siblingData?.layout === 'slider' || siblingData?.layout === 'marquee',
},
fields: [
{
name: 'perView',
type: 'select',
defaultValue: '4',
label: 'Sichtbare Logos',
options: [
{ label: '3', value: '3' },
{ label: '4', value: '4' },
{ label: '5', value: '5' },
{ label: '6', value: '6' },
{ label: 'Auto', value: 'auto' },
],
},
{
name: 'autoplay',
type: 'checkbox',
defaultValue: true,
label: 'Autoplay',
},
{
name: 'speed',
type: 'select',
defaultValue: '3000',
label: 'Geschwindigkeit',
options: [
{ label: 'Langsam', value: '5000' },
{ label: 'Normal', value: '3000' },
{ label: 'Schnell', value: '2000' },
],
admin: {
condition: (_, siblingData) => siblingData?.autoplay,
},
},
{
name: 'pauseOnHover',
type: 'checkbox',
defaultValue: true,
label: 'Bei Hover pausieren',
},
{
name: 'showArrows',
type: 'checkbox',
defaultValue: false,
label: 'Pfeile anzeigen',
},
{
name: 'showDots',
type: 'checkbox',
defaultValue: false,
label: 'Punkte anzeigen',
},
],
},
// Logo-Darstellung
{
name: 'logoStyle',
type: 'group',
label: 'Logo-Darstellung',
fields: [
{
name: 'size',
type: 'select',
defaultValue: 'md',
label: 'Größe',
options: [
{ label: 'Klein', value: 'sm' },
{ label: 'Mittel', value: 'md' },
{ label: 'Groß', value: 'lg' },
],
},
{
name: 'maxHeight',
type: 'select',
defaultValue: '60',
label: 'Max. Höhe',
options: [
{ label: '40px', value: '40' },
{ label: '50px', value: '50' },
{ label: '60px', value: '60' },
{ label: '80px', value: '80' },
{ label: '100px', value: '100' },
],
},
{
name: 'grayscale',
type: 'checkbox',
defaultValue: true,
label: 'Graustufen',
admin: {
description: 'Logos in Graustufen anzeigen',
},
},
{
name: 'colorOnHover',
type: 'checkbox',
defaultValue: true,
label: 'Farbe bei Hover',
admin: {
condition: (_, siblingData) => siblingData?.grayscale,
},
},
{
name: 'opacity',
type: 'select',
defaultValue: '70',
label: 'Deckkraft',
options: [
{ label: '50%', value: '50' },
{ label: '70%', value: '70' },
{ label: '100%', value: '100' },
],
},
{
name: 'hoverEffect',
type: 'select',
defaultValue: 'scale',
label: 'Hover-Effekt',
options: [
{ label: 'Keiner', value: 'none' },
{ label: 'Vergrößern', value: 'scale' },
{ label: 'Aufhellen', value: 'brighten' },
{ label: 'Schatten', value: 'shadow' },
],
},
],
},
// Verhalten
{
name: 'behavior',
type: 'group',
label: 'Verhalten',
fields: [
{
name: 'linkToWebsite',
type: 'checkbox',
defaultValue: false,
label: 'Zur Partner-Website verlinken',
},
{
name: 'openInNewTab',
type: 'checkbox',
defaultValue: true,
label: 'In neuem Tab öffnen',
admin: {
condition: (_, siblingData) => siblingData?.linkToWebsite,
},
},
{
name: 'showTooltip',
type: 'checkbox',
defaultValue: true,
label: 'Tooltip mit Namen',
},
{
name: 'showName',
type: 'checkbox',
defaultValue: false,
label: 'Namen unter Logo anzeigen',
},
],
},
// Styling
{
name: 'style',
type: 'group',
label: 'Darstellung',
fields: [
{
name: 'bg',
type: 'select',
defaultValue: 'none',
label: 'Hintergrund',
options: [
{ label: 'Keiner', value: 'none' },
{ label: 'Hell', value: 'light' },
{ label: 'Weiß', value: 'white' },
{ label: 'Dunkel', value: 'dark' },
],
},
{
name: 'logoBg',
type: 'select',
defaultValue: 'none',
label: 'Logo-Hintergrund',
options: [
{ label: 'Keiner', value: 'none' },
{ label: 'Weiß', value: 'white' },
{ label: 'Hell', value: 'light' },
],
},
{
name: 'logoPadding',
type: 'checkbox',
defaultValue: true,
label: 'Logo-Padding',
},
{
name: 'gap',
type: 'select',
defaultValue: '32',
label: 'Abstand',
options: [
{ label: 'Klein (16px)', value: '16' },
{ label: 'Normal (24px)', value: '24' },
{ label: 'Groß (32px)', value: '32' },
{ label: 'Sehr groß (48px)', value: '48' },
],
},
{
name: 'alignment',
type: 'select',
defaultValue: 'center',
label: 'Ausrichtung',
options: [
{ label: 'Links', value: 'left' },
{ label: 'Mitte', value: 'center' },
{ label: 'Rechts', value: 'right' },
],
},
{
name: 'divider',
type: 'checkbox',
defaultValue: false,
label: 'Trennlinien zwischen Logos',
},
],
},
// CTA
{
name: 'showCTA',
type: 'checkbox',
defaultValue: false,
label: 'CTA anzeigen',
},
{
name: 'ctaText',
type: 'text',
label: 'CTA-Text',
localized: true,
defaultValue: 'Alle Partner ansehen',
admin: {
condition: (_, siblingData) => siblingData?.showCTA,
},
},
{
name: 'ctaLink',
type: 'text',
label: 'CTA-Link',
defaultValue: '/partner',
admin: {
condition: (_, siblingData) => siblingData?.showCTA,
},
},
],
}

588
src/blocks/MapBlock.ts Normal file
View file

@ -0,0 +1,588 @@
import type { Block } from 'payload'
/**
* MapBlock
*
* Zeigt eine interaktive Karte mit konfigurierbaren Markern.
* Unterstützt verschiedene Map-Provider und Marker-Typen.
*/
export const MapBlock: Block = {
slug: 'map-block',
labels: {
singular: 'Karte',
plural: 'Karten',
},
imageURL: '/assets/blocks/map.png',
fields: [
{
name: 'title',
type: 'text',
label: 'Überschrift',
localized: true,
},
{
name: 'subtitle',
type: 'textarea',
label: 'Untertitel',
localized: true,
},
// Datenquelle
{
name: 'source',
type: 'select',
defaultValue: 'locations',
label: 'Marker-Quelle',
options: [
{ label: 'Aus Locations Collection', value: 'locations' },
{ label: 'Manuell definieren', value: 'manual' },
{ label: 'Einzelne Adresse', value: 'single' },
],
},
// Locations-basiert
{
name: 'locationType',
type: 'select',
label: 'Standort-Typ',
hasMany: true,
options: [
{ label: 'Alle', value: 'all' },
{ label: 'Hauptsitz', value: 'headquarters' },
{ label: 'Filiale', value: 'branch' },
{ label: 'Niederlassung', value: 'office' },
{ label: 'Lager/Logistik', value: 'warehouse' },
{ label: 'Produktion', value: 'production' },
{ label: 'Showroom', value: 'showroom' },
{ label: 'Service-Center', value: 'service' },
{ label: 'Partner', value: 'partner' },
],
defaultValue: ['all'],
admin: {
condition: (_, siblingData) => siblingData?.source === 'locations',
},
},
{
name: 'selectedLocations',
type: 'relationship',
relationTo: 'locations',
hasMany: true,
label: 'Standorte auswählen',
admin: {
condition: (_, siblingData) => siblingData?.source === 'locations',
description: 'Optional: Bestimmte Standorte auswählen (sonst alle vom gewählten Typ)',
},
},
// Manuelle Marker
{
name: 'markers',
type: 'array',
label: 'Marker',
admin: {
condition: (_, siblingData) => siblingData?.source === 'manual',
},
fields: [
{
name: 'name',
type: 'text',
required: true,
label: 'Name',
localized: true,
},
{
name: 'address',
type: 'textarea',
label: 'Adresse',
localized: true,
},
{
name: 'lat',
type: 'number',
required: true,
label: 'Breitengrad',
admin: {
step: 0.000001,
},
},
{
name: 'lng',
type: 'number',
required: true,
label: 'Längengrad',
admin: {
step: 0.000001,
},
},
{
name: 'markerType',
type: 'select',
label: 'Marker-Typ',
options: [
{ label: 'Standard', value: 'default' },
{ label: 'Hauptstandort', value: 'main' },
{ label: 'Highlight', value: 'highlight' },
{ label: 'Custom', value: 'custom' },
],
},
{
name: 'customIcon',
type: 'upload',
relationTo: 'media',
label: 'Custom Icon',
admin: {
condition: (_, siblingData) => siblingData?.markerType === 'custom',
},
},
{
name: 'popupContent',
type: 'richText',
label: 'Popup-Inhalt',
localized: true,
},
{
name: 'link',
type: 'text',
label: 'Link',
},
],
},
// Einzelne Adresse
{
name: 'singleAddress',
type: 'group',
label: 'Adresse',
admin: {
condition: (_, siblingData) => siblingData?.source === 'single',
},
fields: [
{
name: 'name',
type: 'text',
label: 'Name/Titel',
localized: true,
},
{
name: 'street',
type: 'text',
label: 'Straße',
},
{
name: 'zip',
type: 'text',
label: 'PLZ',
},
{
name: 'city',
type: 'text',
label: 'Stadt',
},
{
name: 'country',
type: 'text',
defaultValue: 'Deutschland',
label: 'Land',
},
{
name: 'lat',
type: 'number',
label: 'Breitengrad',
admin: {
step: 0.000001,
description: 'Optional - wird sonst per Geocoding ermittelt',
},
},
{
name: 'lng',
type: 'number',
label: 'Längengrad',
admin: {
step: 0.000001,
},
},
],
},
// Map Provider
{
name: 'provider',
type: 'select',
defaultValue: 'osm',
label: 'Karten-Anbieter',
options: [
{ label: 'OpenStreetMap', value: 'osm' },
{ label: 'Google Maps', value: 'google' },
{ label: 'Mapbox', value: 'mapbox' },
{ label: 'Leaflet (Custom Tiles)', value: 'leaflet' },
],
},
{
name: 'mapStyle',
type: 'select',
defaultValue: 'default',
label: 'Karten-Stil',
options: [
{ label: 'Standard', value: 'default' },
{ label: 'Hell', value: 'light' },
{ label: 'Dunkel', value: 'dark' },
{ label: 'Satellite', value: 'satellite' },
{ label: 'Hybrid', value: 'hybrid' },
{ label: 'Terrain', value: 'terrain' },
],
},
// Karten-Einstellungen
{
name: 'mapSettings',
type: 'group',
label: 'Karten-Einstellungen',
fields: [
{
name: 'height',
type: 'select',
defaultValue: '400',
label: 'Höhe',
options: [
{ label: 'Klein (300px)', value: '300' },
{ label: 'Normal (400px)', value: '400' },
{ label: 'Groß (500px)', value: '500' },
{ label: 'Sehr groß (600px)', value: '600' },
{ label: 'Vollbild-Höhe', value: 'fullscreen' },
],
},
{
name: 'zoom',
type: 'number',
defaultValue: 12,
min: 1,
max: 20,
label: 'Zoom-Level',
admin: {
description: '1 = Welt, 20 = Gebäude-Details',
},
},
{
name: 'autoFit',
type: 'checkbox',
defaultValue: true,
label: 'Zoom automatisch anpassen',
admin: {
description: 'Zoom so anpassen, dass alle Marker sichtbar sind',
},
},
{
name: 'centerLat',
type: 'number',
label: 'Mittelpunkt Breitengrad',
admin: {
step: 0.000001,
description: 'Optional - wird automatisch berechnet wenn leer',
},
},
{
name: 'centerLng',
type: 'number',
label: 'Mittelpunkt Längengrad',
admin: {
step: 0.000001,
},
},
],
},
// Interaktion
{
name: 'interaction',
type: 'group',
label: 'Interaktion',
fields: [
{
name: 'scrollZoom',
type: 'checkbox',
defaultValue: false,
label: 'Zoom mit Mausrad',
},
{
name: 'dragging',
type: 'checkbox',
defaultValue: true,
label: 'Verschieben erlauben',
},
{
name: 'zoomControl',
type: 'checkbox',
defaultValue: true,
label: 'Zoom-Buttons',
},
{
name: 'fullscreenControl',
type: 'checkbox',
defaultValue: false,
label: 'Vollbild-Button',
},
{
name: 'locateControl',
type: 'checkbox',
defaultValue: false,
label: 'Standort-Button',
admin: {
description: 'User-Standort ermitteln',
},
},
],
},
// Marker-Darstellung
{
name: 'markerStyle',
type: 'group',
label: 'Marker-Darstellung',
fields: [
{
name: 'type',
type: 'select',
defaultValue: 'pin',
label: 'Marker-Typ',
options: [
{ label: 'Pin', value: 'pin' },
{ label: 'Punkt', value: 'dot' },
{ label: 'Icon', value: 'icon' },
{ label: 'Custom', value: 'custom' },
],
},
{
name: 'color',
type: 'text',
defaultValue: '#3B82F6',
label: 'Marker-Farbe',
admin: {
description: 'HEX-Farbcode, z.B. #3B82F6',
},
},
{
name: 'size',
type: 'select',
defaultValue: 'md',
label: 'Größe',
options: [
{ label: 'Klein', value: 'sm' },
{ label: 'Mittel', value: 'md' },
{ label: 'Groß', value: 'lg' },
],
},
{
name: 'clustering',
type: 'checkbox',
defaultValue: true,
label: 'Marker clustern',
admin: {
description: 'Nahe beieinander liegende Marker gruppieren',
},
},
{
name: 'clusterRadius',
type: 'number',
defaultValue: 50,
label: 'Cluster-Radius',
admin: {
condition: (_, siblingData) => siblingData?.clustering,
},
},
],
},
// Popup-Einstellungen
{
name: 'popup',
type: 'group',
label: 'Popup-Einstellungen',
fields: [
{
name: 'show',
type: 'checkbox',
defaultValue: true,
label: 'Popups anzeigen',
},
{
name: 'trigger',
type: 'select',
defaultValue: 'click',
label: 'Öffnen bei',
options: [
{ label: 'Klick', value: 'click' },
{ label: 'Hover', value: 'hover' },
],
admin: {
condition: (_, siblingData) => siblingData?.show,
},
},
{
name: 'showAddress',
type: 'checkbox',
defaultValue: true,
label: 'Adresse anzeigen',
admin: {
condition: (_, siblingData) => siblingData?.show,
},
},
{
name: 'showDirectionsLink',
type: 'checkbox',
defaultValue: true,
label: 'Routenplaner-Link',
admin: {
condition: (_, siblingData) => siblingData?.show,
},
},
{
name: 'showPhone',
type: 'checkbox',
defaultValue: true,
label: 'Telefon anzeigen',
admin: {
condition: (_, siblingData) => siblingData?.show,
},
},
{
name: 'showOpeningHours',
type: 'checkbox',
defaultValue: false,
label: 'Öffnungszeiten anzeigen',
admin: {
condition: (_, siblingData) => siblingData?.show,
},
},
{
name: 'showDetailLink',
type: 'checkbox',
defaultValue: true,
label: 'Detail-Link anzeigen',
admin: {
condition: (_, siblingData) => siblingData?.show,
},
},
],
},
// Sidebar/Liste
{
name: 'sidebar',
type: 'group',
label: 'Standort-Liste',
fields: [
{
name: 'show',
type: 'checkbox',
defaultValue: false,
label: 'Liste neben Karte',
},
{
name: 'position',
type: 'select',
defaultValue: 'right',
label: 'Position',
options: [
{ label: 'Links', value: 'left' },
{ label: 'Rechts', value: 'right' },
{ label: 'Unten', value: 'bottom' },
],
admin: {
condition: (_, siblingData) => siblingData?.show,
},
},
{
name: 'width',
type: 'select',
defaultValue: '300',
label: 'Breite',
options: [
{ label: 'Schmal (250px)', value: '250' },
{ label: 'Normal (300px)', value: '300' },
{ label: 'Breit (400px)', value: '400' },
],
admin: {
condition: (_, siblingData) =>
siblingData?.show && siblingData?.position !== 'bottom',
},
},
{
name: 'searchable',
type: 'checkbox',
defaultValue: true,
label: 'Suchfeld in Liste',
admin: {
condition: (_, siblingData) => siblingData?.show,
},
},
{
name: 'clickToCenter',
type: 'checkbox',
defaultValue: true,
label: 'Klick zentriert Karte',
admin: {
condition: (_, siblingData) => siblingData?.show,
},
},
],
},
// Styling
{
name: 'style',
type: 'group',
label: 'Darstellung',
fields: [
{
name: 'rounded',
type: 'checkbox',
defaultValue: true,
label: 'Abgerundete Ecken',
},
{
name: 'shadow',
type: 'checkbox',
defaultValue: true,
label: 'Schatten',
},
{
name: 'border',
type: 'checkbox',
defaultValue: false,
label: 'Rahmen',
},
{
name: 'padding',
type: 'select',
defaultValue: 'none',
label: 'Außenabstand',
options: [
{ label: 'Keiner', value: 'none' },
{ label: 'Klein', value: 'sm' },
{ label: 'Normal', value: 'md' },
{ label: 'Groß', value: 'lg' },
],
},
],
},
// Fallback
{
name: 'fallback',
type: 'group',
label: 'Fallback',
fields: [
{
name: 'showStaticImage',
type: 'checkbox',
defaultValue: false,
label: 'Statisches Bild als Fallback',
},
{
name: 'staticImage',
type: 'upload',
relationTo: 'media',
label: 'Fallback-Bild',
admin: {
condition: (_, siblingData) => siblingData?.showStaticImage,
},
},
{
name: 'noJsMessage',
type: 'text',
label: 'Nachricht ohne JavaScript',
localized: true,
defaultValue: 'Bitte aktivieren Sie JavaScript, um die Karte anzuzeigen.',
},
],
},
],
}

509
src/blocks/PricingBlock.ts Normal file
View file

@ -0,0 +1,509 @@
import type { Block } from 'payload'
/**
* Pricing Block
*
* Preistabellen für Produkte, Dienstleistungen oder Abonnements.
* Unterstützt verschiedene Layouts, Vergleichstabellen und
* Highlight-Features für empfohlene Pläne.
*/
export const PricingBlock: Block = {
slug: 'pricing',
labels: {
singular: 'Preistabelle',
plural: 'Preistabellen',
},
fields: [
{
name: 'title',
type: 'text',
label: 'Überschrift',
localized: true,
},
{
name: 'subtitle',
type: 'text',
label: 'Untertitel',
localized: true,
},
{
name: 'description',
type: 'textarea',
label: 'Beschreibung',
localized: true,
},
// Preismodell
{
name: 'pricingType',
type: 'select',
defaultValue: 'one-time',
label: 'Preismodell',
options: [
{ label: 'Einmalig', value: 'one-time' },
{ label: 'Monatlich', value: 'monthly' },
{ label: 'Jährlich', value: 'yearly' },
{ label: 'Monatlich/Jährlich Toggle', value: 'toggle' },
{ label: 'Individuell', value: 'custom' },
],
},
{
name: 'currency',
type: 'text',
defaultValue: '€',
label: 'Währung',
},
{
name: 'showCurrencyBefore',
type: 'checkbox',
defaultValue: true,
label: 'Währung vor dem Preis',
},
// Toggle-Optionen für Monatlich/Jährlich
{
name: 'toggleOptions',
type: 'group',
label: 'Toggle-Optionen',
admin: {
condition: (data, siblingData) => siblingData?.pricingType === 'toggle',
},
fields: [
{
name: 'monthlyLabel',
type: 'text',
defaultValue: 'Monatlich',
label: 'Monatlich Label',
localized: true,
},
{
name: 'yearlyLabel',
type: 'text',
defaultValue: 'Jährlich',
label: 'Jährlich Label',
localized: true,
},
{
name: 'yearlyDiscount',
type: 'text',
label: 'Rabatt-Badge',
localized: true,
admin: {
description: 'z.B. "2 Monate gratis"',
},
},
{
name: 'defaultToYearly',
type: 'checkbox',
defaultValue: true,
label: 'Standard: Jährlich',
},
],
},
// Preispläne
{
name: 'plans',
type: 'array',
label: 'Preispläne',
minRows: 1,
maxRows: 6,
fields: [
{
name: 'name',
type: 'text',
required: true,
label: 'Plan-Name',
localized: true,
admin: {
description: 'z.B. "Basic", "Pro", "Enterprise"',
},
},
{
name: 'subtitle',
type: 'text',
label: 'Untertitel',
localized: true,
admin: {
description: 'z.B. "Für Einsteiger"',
},
},
{
name: 'description',
type: 'textarea',
label: 'Beschreibung',
localized: true,
},
// Preis
{
type: 'row',
fields: [
{
name: 'price',
type: 'number',
label: 'Preis',
admin: {
width: '25%',
},
},
{
name: 'priceMonthly',
type: 'number',
label: 'Preis Monatlich',
admin: {
width: '25%',
condition: (data, siblingData, { blockData }) =>
blockData?.pricingType === 'toggle',
},
},
{
name: 'priceYearly',
type: 'number',
label: 'Preis Jährlich',
admin: {
width: '25%',
condition: (data, siblingData, { blockData }) =>
blockData?.pricingType === 'toggle',
},
},
{
name: 'priceSuffix',
type: 'text',
label: 'Preis-Suffix',
localized: true,
admin: {
width: '25%',
description: 'z.B. "/Monat", "/Nutzer"',
},
},
],
},
{
name: 'originalPrice',
type: 'number',
label: 'Originalpreis (durchgestrichen)',
admin: {
description: 'Für Rabatt-Darstellung',
},
},
{
name: 'priceNote',
type: 'text',
label: 'Preis-Hinweis',
localized: true,
admin: {
description: 'z.B. "zzgl. MwSt.", "Bei jährlicher Zahlung"',
},
},
{
name: 'customPriceText',
type: 'text',
label: 'Individueller Preis-Text',
localized: true,
admin: {
description: 'z.B. "Auf Anfrage", "Individuell"',
condition: (data, siblingData, { blockData }) =>
blockData?.pricingType === 'custom',
},
},
// Features
{
name: 'features',
type: 'array',
label: 'Features',
fields: [
{
name: 'text',
type: 'text',
required: true,
label: 'Feature',
localized: true,
},
{
name: 'included',
type: 'checkbox',
defaultValue: true,
label: 'Enthalten',
},
{
name: 'highlight',
type: 'checkbox',
defaultValue: false,
label: 'Hervorheben',
},
{
name: 'tooltip',
type: 'text',
label: 'Tooltip',
localized: true,
},
],
},
// CTA
{
name: 'ctaText',
type: 'text',
defaultValue: 'Jetzt starten',
label: 'Button-Text',
localized: true,
},
{
name: 'ctaLink',
type: 'text',
label: 'Button-Link',
},
{
name: 'ctaStyle',
type: 'select',
defaultValue: 'primary',
label: 'Button-Stil',
options: [
{ label: 'Primär', value: 'primary' },
{ label: 'Sekundär', value: 'secondary' },
{ label: 'Outline', value: 'outline' },
],
},
// Hervorhebung
{
name: 'isPopular',
type: 'checkbox',
defaultValue: false,
label: 'Als "Beliebt" markieren',
},
{
name: 'popularLabel',
type: 'text',
defaultValue: 'Beliebt',
label: 'Beliebt-Label',
localized: true,
admin: {
condition: (data, siblingData) => siblingData?.isPopular,
},
},
{
name: 'isRecommended',
type: 'checkbox',
defaultValue: false,
label: 'Als "Empfohlen" markieren',
},
{
name: 'recommendedLabel',
type: 'text',
defaultValue: 'Empfohlen',
label: 'Empfohlen-Label',
localized: true,
admin: {
condition: (data, siblingData) => siblingData?.isRecommended,
},
},
// Styling
{
name: 'accentColor',
type: 'select',
label: 'Akzentfarbe',
options: [
{ label: 'Standard', value: 'default' },
{ label: 'Primär', value: 'primary' },
{ label: 'Sekundär', value: 'secondary' },
{ label: 'Grün', value: 'green' },
{ label: 'Blau', value: 'blue' },
{ label: 'Lila', value: 'purple' },
],
},
{
name: 'icon',
type: 'upload',
relationTo: 'media',
label: 'Icon/Logo',
},
],
},
// Vergleichstabelle
{
name: 'showComparison',
type: 'checkbox',
defaultValue: false,
label: 'Vergleichstabelle anzeigen',
},
{
name: 'comparisonFeatures',
type: 'array',
label: 'Vergleichs-Features',
admin: {
condition: (data, siblingData) => siblingData?.showComparison,
},
fields: [
{
name: 'category',
type: 'text',
label: 'Kategorie',
localized: true,
},
{
name: 'feature',
type: 'text',
required: true,
label: 'Feature',
localized: true,
},
{
name: 'tooltip',
type: 'text',
label: 'Tooltip',
localized: true,
},
{
name: 'values',
type: 'array',
label: 'Werte pro Plan',
fields: [
{
name: 'planIndex',
type: 'number',
label: 'Plan-Index (0-basiert)',
},
{
name: 'value',
type: 'text',
label: 'Wert',
localized: true,
admin: {
description: '✓, ✗, oder Text wie "10 GB", "Unbegrenzt"',
},
},
],
},
],
},
// Layout
{
name: 'layout',
type: 'select',
defaultValue: 'cards',
label: 'Layout',
options: [
{ label: 'Karten', value: 'cards' },
{ label: 'Tabelle', value: 'table' },
{ label: 'Kompakt', value: 'compact' },
],
},
{
name: 'alignment',
type: 'select',
defaultValue: 'center',
label: 'Ausrichtung',
options: [
{ label: 'Links', value: 'left' },
{ label: 'Zentriert', value: 'center' },
{ label: 'Rechts', value: 'right' },
],
},
{
name: 'highlightPopular',
type: 'checkbox',
defaultValue: true,
label: 'Beliebten Plan hervorheben',
},
// Geld-zurück-Garantie etc.
{
name: 'guarantee',
type: 'group',
label: 'Garantie/Trust-Elemente',
fields: [
{
name: 'show',
type: 'checkbox',
defaultValue: false,
label: 'Garantie anzeigen',
},
{
name: 'text',
type: 'text',
defaultValue: '30 Tage Geld-zurück-Garantie',
label: 'Garantie-Text',
localized: true,
admin: {
condition: (data, siblingData) => siblingData?.show,
},
},
{
name: 'icon',
type: 'select',
label: 'Icon',
options: [
{ label: 'Schild', value: 'shield' },
{ label: 'Häkchen', value: 'check' },
{ label: 'Stern', value: 'star' },
{ label: 'Schloss', value: 'lock' },
],
admin: {
condition: (data, siblingData) => siblingData?.show,
},
},
],
},
// FAQ
{
name: 'showFAQ',
type: 'checkbox',
defaultValue: false,
label: 'FAQ anzeigen',
},
{
name: 'faqTitle',
type: 'text',
defaultValue: 'Häufig gestellte Fragen',
label: 'FAQ-Überschrift',
localized: true,
admin: {
condition: (data, siblingData) => siblingData?.showFAQ,
},
},
{
name: 'faqItems',
type: 'array',
label: 'FAQ-Einträge',
admin: {
condition: (data, siblingData) => siblingData?.showFAQ,
},
fields: [
{
name: 'question',
type: 'text',
required: true,
label: 'Frage',
localized: true,
},
{
name: 'answer',
type: 'richText',
required: true,
label: 'Antwort',
localized: true,
},
],
},
// Styling
{
name: 'backgroundColor',
type: 'select',
defaultValue: 'white',
label: 'Hintergrund',
options: [
{ label: 'Weiß', value: 'white' },
{ label: 'Hell (Grau)', value: 'light' },
{ label: 'Dunkel', value: 'dark' },
{ label: 'Gradient', value: 'gradient' },
],
},
{
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: 'Glass', value: 'glass' },
],
},
],
}

372
src/blocks/StatsBlock.ts Normal file
View file

@ -0,0 +1,372 @@
import type { Block } from 'payload'
/**
* StatsBlock
*
* Zeigt Kennzahlen und Statistiken in verschiedenen Layouts.
* Unterstützt Animationen und verschiedene Darstellungsformen.
*/
export const StatsBlock: Block = {
slug: 'stats-block',
labels: {
singular: 'Statistiken',
plural: 'Statistiken',
},
imageURL: '/assets/blocks/stats.png',
fields: [
{
name: 'title',
type: 'text',
label: 'Überschrift',
localized: true,
},
{
name: 'subtitle',
type: 'textarea',
label: 'Untertitel',
localized: true,
},
// Statistiken
{
name: 'stats',
type: 'array',
required: true,
minRows: 1,
maxRows: 8,
label: 'Statistiken',
fields: [
{
name: 'value',
type: 'text',
required: true,
label: 'Wert',
admin: {
description: 'z.B. "500+", "99%", "24/7", "10.000"',
},
},
{
name: 'numericValue',
type: 'number',
label: 'Numerischer Wert',
admin: {
description: 'Für Zähl-Animation (z.B. 500 für "500+")',
},
},
{
name: 'prefix',
type: 'text',
label: 'Präfix',
admin: {
description: 'z.B. "€", "+"',
},
},
{
name: 'suffix',
type: 'text',
label: 'Suffix',
admin: {
description: 'z.B. "+", "%", "k", "Mio."',
},
},
{
name: 'label',
type: 'text',
required: true,
label: 'Beschreibung',
localized: true,
admin: {
description: 'z.B. "Zufriedene Kunden", "Erfolgsquote"',
},
},
{
name: 'description',
type: 'textarea',
label: 'Zusatztext',
localized: true,
admin: {
description: 'Optionaler erklärender Text',
},
},
{
name: 'icon',
type: 'select',
label: 'Icon',
options: [
{ label: 'Keins', value: 'none' },
{ label: 'Nutzer', value: 'users' },
{ label: 'Stern', value: 'star' },
{ label: 'Herz', value: 'heart' },
{ label: 'Check', value: 'check' },
{ label: 'Trophäe', value: 'trophy' },
{ label: 'Chart', value: 'chart' },
{ label: 'Uhr', value: 'clock' },
{ label: 'Kalender', value: 'calendar' },
{ label: 'Globus', value: 'globe' },
{ label: 'Gebäude', value: 'building' },
{ label: 'Dokument', value: 'document' },
{ label: 'Ziel', value: 'target' },
{ label: 'Rakete', value: 'rocket' },
{ label: 'Handshake', value: 'handshake' },
],
},
{
name: 'color',
type: 'select',
label: 'Akzentfarbe',
options: [
{ label: 'Standard', value: 'default' },
{ label: 'Primary', value: 'primary' },
{ label: 'Grün', value: 'green' },
{ label: 'Blau', value: 'blue' },
{ label: 'Orange', value: 'orange' },
{ label: 'Rot', value: 'red' },
],
},
],
},
// Layout
{
name: 'layout',
type: 'select',
defaultValue: 'row',
label: 'Layout',
options: [
{ label: 'Horizontal', value: 'row' },
{ label: 'Grid', value: 'grid' },
{ label: 'Karten', value: 'cards' },
{ label: 'Vertikal', value: 'column' },
{ label: 'Inline (kompakt)', value: 'inline' },
{ label: 'Feature (groß)', value: 'feature' },
],
},
{
name: 'columns',
type: 'select',
defaultValue: '4',
label: 'Spalten',
options: [
{ label: '2 Spalten', value: '2' },
{ label: '3 Spalten', value: '3' },
{ label: '4 Spalten', value: '4' },
{ label: 'Auto', value: 'auto' },
],
admin: {
condition: (_, siblingData) =>
siblingData?.layout === 'grid' || siblingData?.layout === 'cards',
},
},
{
name: 'alignment',
type: 'select',
defaultValue: 'center',
label: 'Ausrichtung',
options: [
{ label: 'Links', value: 'left' },
{ label: 'Mitte', value: 'center' },
{ label: 'Rechts', value: 'right' },
],
},
// Animation
{
name: 'animation',
type: 'group',
label: 'Animation',
fields: [
{
name: 'countUp',
type: 'checkbox',
defaultValue: true,
label: 'Zähl-Animation',
admin: {
description: 'Zahlen hochzählen (benötigt numericValue)',
},
},
{
name: 'duration',
type: 'select',
defaultValue: '2000',
label: 'Dauer',
options: [
{ label: 'Schnell (1s)', value: '1000' },
{ label: 'Normal (2s)', value: '2000' },
{ label: 'Langsam (3s)', value: '3000' },
],
admin: {
condition: (_, siblingData) => siblingData?.countUp,
},
},
{
name: 'trigger',
type: 'select',
defaultValue: 'viewport',
label: 'Trigger',
options: [
{ label: 'Beim Scrollen sichtbar', value: 'viewport' },
{ label: 'Sofort', value: 'immediate' },
],
admin: {
condition: (_, siblingData) => siblingData?.countUp,
},
},
{
name: 'stagger',
type: 'checkbox',
defaultValue: true,
label: 'Versetzt animieren',
admin: {
description: 'Statistiken nacheinander einblenden',
condition: (_, siblingData) => siblingData?.countUp,
},
},
],
},
// Styling
{
name: 'style',
type: 'group',
label: 'Darstellung',
fields: [
{
name: 'bg',
type: 'select',
defaultValue: 'none',
label: 'Hintergrund',
options: [
{ label: 'Keiner', value: 'none' },
{ label: 'Hell', value: 'light' },
{ label: 'Dunkel', value: 'dark' },
{ label: 'Primary', value: 'primary' },
{ label: 'Gradient', value: 'gradient' },
],
},
{
name: 'bgImage',
type: 'upload',
relationTo: 'media',
label: 'Hintergrundbild',
admin: {
condition: (_, siblingData) => siblingData?.bg === 'none',
},
},
{
name: 'bgOverlay',
type: 'select',
defaultValue: 'dark',
label: 'Overlay',
options: [
{ label: 'Keins', value: 'none' },
{ label: 'Dunkel', value: 'dark' },
{ label: 'Hell', value: 'light' },
{ label: 'Primary', value: 'primary' },
],
admin: {
condition: (_, siblingData) => siblingData?.bgImage,
},
},
{
name: 'textColor',
type: 'select',
defaultValue: 'auto',
label: 'Textfarbe',
options: [
{ label: 'Auto', value: 'auto' },
{ label: 'Dunkel', value: 'dark' },
{ label: 'Hell', value: 'light' },
],
},
{
name: 'valueSize',
type: 'select',
defaultValue: 'xl',
label: 'Zahlen-Größe',
options: [
{ label: 'Normal', value: 'base' },
{ label: 'Groß', value: 'lg' },
{ label: 'Sehr groß', value: 'xl' },
{ label: 'Riesig', value: '2xl' },
],
},
{
name: 'valueWeight',
type: 'select',
defaultValue: 'bold',
label: 'Zahlen-Gewicht',
options: [
{ label: 'Normal', value: 'normal' },
{ label: 'Medium', value: 'medium' },
{ label: 'Bold', value: 'bold' },
{ label: 'Extra Bold', value: 'extrabold' },
],
},
{
name: 'showIcon',
type: 'checkbox',
defaultValue: true,
label: 'Icons anzeigen',
},
{
name: 'iconPosition',
type: 'select',
defaultValue: 'top',
label: 'Icon-Position',
options: [
{ label: 'Oben', value: 'top' },
{ label: 'Links', value: 'left' },
{ label: 'In Zahl', value: 'inline' },
],
admin: {
condition: (_, siblingData) => siblingData?.showIcon,
},
},
{
name: 'dividers',
type: 'checkbox',
defaultValue: false,
label: 'Trennlinien',
},
{
name: 'cardBorder',
type: 'checkbox',
defaultValue: false,
label: 'Karten-Rahmen',
admin: {
condition: (_, siblingData) => siblingData?.layout === 'cards',
},
},
{
name: 'cardShadow',
type: 'checkbox',
defaultValue: true,
label: 'Karten-Schatten',
admin: {
condition: (_, siblingData) => siblingData?.layout === 'cards',
},
},
{
name: 'gap',
type: 'select',
defaultValue: '32',
label: 'Abstand',
options: [
{ label: 'Klein (16px)', value: '16' },
{ label: 'Normal (24px)', value: '24' },
{ label: 'Groß (32px)', value: '32' },
{ label: 'Sehr groß (48px)', value: '48' },
],
},
{
name: 'padding',
type: 'select',
defaultValue: 'md',
label: 'Innenabstand',
options: [
{ label: 'Keiner', value: 'none' },
{ label: 'Klein', value: 'sm' },
{ label: 'Normal', value: 'md' },
{ label: 'Groß', value: 'lg' },
],
},
],
},
],
}

381
src/blocks/TabsBlock.ts Normal file
View file

@ -0,0 +1,381 @@
import type { Block } from 'payload'
/**
* Tabs Block
*
* Tabbed Content für organisierte Informationen.
* Unterstützt verschiedene Tab-Stile, Icons und
* kann andere Blocks als Content enthalten.
*/
export const TabsBlock: Block = {
slug: 'tabs',
labels: {
singular: 'Tabs',
plural: 'Tabs',
},
fields: [
{
name: 'title',
type: 'text',
label: 'Überschrift',
localized: true,
},
{
name: 'subtitle',
type: 'text',
label: 'Untertitel',
localized: true,
},
// Tabs
{
name: 'tabs',
type: 'array',
label: 'Tabs',
minRows: 2,
maxRows: 10,
fields: [
{
name: 'label',
type: 'text',
required: true,
label: 'Tab-Titel',
localized: true,
},
{
name: 'icon',
type: 'select',
label: 'Icon',
options: [
{ label: 'Keines', value: 'none' },
{ label: 'Info', value: 'info' },
{ label: 'Stern', value: 'star' },
{ label: 'Herz', value: 'heart' },
{ label: 'Häkchen', value: 'check' },
{ label: 'Einstellungen', value: 'settings' },
{ label: 'Benutzer', value: 'user' },
{ label: 'Dokument', value: 'document' },
{ label: 'Bild', value: 'image' },
{ label: 'Video', value: 'video' },
{ label: 'Code', value: 'code' },
{ label: 'Chart', value: 'chart' },
{ label: 'Kalender', value: 'calendar' },
{ label: 'Standort', value: 'location' },
{ label: 'E-Mail', value: 'email' },
{ label: 'Telefon', value: 'phone' },
],
},
{
name: 'customIcon',
type: 'upload',
relationTo: 'media',
label: 'Eigenes Icon',
admin: {
description: 'Überschreibt das ausgewählte Icon',
},
},
{
name: 'badge',
type: 'text',
label: 'Badge',
admin: {
description: 'z.B. "Neu", "3", "Beta"',
},
},
// Content
{
name: 'contentType',
type: 'select',
defaultValue: 'richtext',
label: 'Content-Typ',
options: [
{ label: 'Rich Text', value: 'richtext' },
{ label: 'Bild & Text', value: 'image-text' },
{ label: 'Feature-Liste', value: 'features' },
{ label: 'Code', value: 'code' },
{ label: 'Embed', value: 'embed' },
],
},
// Rich Text Content
{
name: 'content',
type: 'richText',
label: 'Inhalt',
localized: true,
admin: {
condition: (data, siblingData) => siblingData?.contentType === 'richtext',
},
},
// Image & Text Content
{
name: 'imgTxt',
type: 'group',
label: 'Bild & Text',
admin: {
condition: (data, siblingData) => siblingData?.contentType === 'image-text',
},
fields: [
{
name: 'img',
type: 'upload',
relationTo: 'media',
label: 'Bild',
},
{
name: 'imgPos',
type: 'select',
defaultValue: 'left',
label: 'Bild-Position',
options: [
{ label: 'Links', value: 'left' },
{ label: 'Rechts', value: 'right' },
{ label: 'Oben', value: 'top' },
],
},
{
name: 'text',
type: 'richText',
label: 'Text',
localized: true,
},
],
},
// Features List
{
name: 'features',
type: 'array',
label: 'Features',
admin: {
condition: (data, siblingData) => siblingData?.contentType === 'features',
},
fields: [
{
name: 'title',
type: 'text',
required: true,
label: 'Titel',
localized: true,
},
{
name: 'description',
type: 'textarea',
label: 'Beschreibung',
localized: true,
},
{
name: 'icon',
type: 'select',
label: 'Icon',
options: [
{ label: 'Häkchen', value: 'check' },
{ label: 'Stern', value: 'star' },
{ label: 'Pfeil', value: 'arrow' },
{ label: 'Punkt', value: 'dot' },
{ label: 'Nummer', value: 'number' },
],
},
],
},
// Code Content
{
name: 'code',
type: 'group',
label: 'Code',
admin: {
condition: (data, siblingData) => siblingData?.contentType === 'code',
},
fields: [
{
name: 'language',
type: 'select',
label: 'Sprache',
options: [
{ label: 'JavaScript', value: 'javascript' },
{ label: 'TypeScript', value: 'typescript' },
{ label: 'HTML', value: 'html' },
{ label: 'CSS', value: 'css' },
{ label: 'JSON', value: 'json' },
{ label: 'Python', value: 'python' },
{ label: 'Bash', value: 'bash' },
{ label: 'SQL', value: 'sql' },
],
},
{
name: 'code',
type: 'code',
label: 'Code',
},
{
name: 'showLineNumbers',
type: 'checkbox',
defaultValue: true,
label: 'Zeilennummern anzeigen',
},
],
},
// Embed Content
{
name: 'embed',
type: 'group',
label: 'Embed',
admin: {
condition: (data, siblingData) => siblingData?.contentType === 'embed',
},
fields: [
{
name: 'type',
type: 'select',
label: 'Typ',
options: [
{ label: 'YouTube', value: 'youtube' },
{ label: 'Vimeo', value: 'vimeo' },
{ label: 'iFrame', value: 'iframe' },
],
},
{
name: 'url',
type: 'text',
label: 'URL',
},
{
name: 'aspectRatio',
type: 'select',
defaultValue: '16:9',
label: 'Seitenverhältnis',
options: [
{ label: '16:9', value: '16:9' },
{ label: '4:3', value: '4:3' },
{ label: '1:1', value: '1:1' },
],
},
],
},
],
},
// Tab Style
{
name: 'tabStyle',
type: 'select',
defaultValue: 'underline',
label: 'Tab-Stil',
options: [
{ label: 'Unterstrichen', value: 'underline' },
{ label: 'Boxed', value: 'boxed' },
{ label: 'Pills', value: 'pills' },
{ label: 'Buttons', value: 'buttons' },
{ label: 'Vertikal', value: 'vertical' },
],
},
{
name: 'tabPosition',
type: 'select',
defaultValue: 'top',
label: 'Tab-Position',
options: [
{ label: 'Oben', value: 'top' },
{ label: 'Links', value: 'left' },
{ label: 'Rechts', value: 'right' },
{ label: 'Unten', value: 'bottom' },
],
admin: {
condition: (data, siblingData) => siblingData?.tabStyle !== 'vertical',
},
},
{
name: 'tabAlignment',
type: 'select',
defaultValue: 'left',
label: 'Tab-Ausrichtung',
options: [
{ label: 'Links', value: 'left' },
{ label: 'Zentriert', value: 'center' },
{ label: 'Rechts', value: 'right' },
{ label: 'Gleichmäßig verteilt', value: 'stretch' },
],
admin: {
condition: (data, siblingData) =>
siblingData?.tabPosition === 'top' || siblingData?.tabPosition === 'bottom',
},
},
// Verhalten
{
name: 'defaultTab',
type: 'number',
defaultValue: 0,
min: 0,
label: 'Standard-Tab (Index)',
admin: {
description: '0 = erster Tab',
},
},
{
name: 'allowKeyboardNavigation',
type: 'checkbox',
defaultValue: true,
label: 'Tastatur-Navigation',
},
{
name: 'animated',
type: 'checkbox',
defaultValue: true,
label: 'Animierter Übergang',
},
{
name: 'lazy',
type: 'checkbox',
defaultValue: false,
label: 'Lazy Loading',
admin: {
description: 'Tab-Inhalte erst bei Aktivierung laden',
},
},
// Mobile
{
name: 'mobileStyle',
type: 'select',
defaultValue: 'scroll',
label: 'Mobile-Ansicht',
options: [
{ label: 'Scrollbar', value: 'scroll' },
{ label: 'Dropdown', value: 'dropdown' },
{ label: 'Accordion', value: 'accordion' },
{ label: 'Gestapelt', value: 'stacked' },
],
},
// Styling
{
name: 'backgroundColor',
type: 'select',
defaultValue: 'white',
label: 'Hintergrund',
options: [
{ label: 'Weiß', value: 'white' },
{ label: 'Hell (Grau)', value: 'light' },
{ label: 'Dunkel', value: 'dark' },
{ label: 'Transparent', value: 'transparent' },
],
},
{
name: 'contentBackground',
type: 'select',
defaultValue: 'white',
label: 'Content-Hintergrund',
options: [
{ label: 'Weiß', value: 'white' },
{ label: 'Hell (Grau)', value: 'light' },
{ label: 'Transparent', value: 'transparent' },
],
},
{
name: 'showBorder',
type: 'checkbox',
defaultValue: true,
label: 'Rahmen anzeigen',
},
{
name: 'fullWidth',
type: 'checkbox',
defaultValue: false,
label: 'Volle Breite',
},
],
}

View file

@ -29,3 +29,18 @@ export { TableOfContentsBlock } from './TableOfContentsBlock'
// Team Blocks // Team Blocks
export { TeamFilterBlock } from './TeamFilterBlock' export { TeamFilterBlock } from './TeamFilterBlock'
export { OrgChartBlock } from './OrgChartBlock' export { OrgChartBlock } from './OrgChartBlock'
// New Feature Blocks
export { LocationsBlock } from './LocationsBlock'
export { LogoGridBlock } from './LogoGridBlock'
export { StatsBlock } from './StatsBlock'
export { JobsBlock } from './JobsBlock'
export { DownloadsBlock } from './DownloadsBlock'
export { MapBlock } from './MapBlock'
// Events & Interactive Blocks
export { EventsBlock } from './EventsBlock'
export { PricingBlock } from './PricingBlock'
export { TabsBlock } from './TabsBlock'
export { AccordionBlock } from './AccordionBlock'
export { ComparisonBlock } from './ComparisonBlock'

View file

@ -0,0 +1,254 @@
import type { CollectionConfig } from 'payload'
import { authenticatedOnly, tenantScopedPublicRead } from '../lib/tenantAccess'
/**
* Downloads Collection
*
* Downloadbare Dateien wie PDFs, Broschüren, Formulare, etc.
* Mit Kategorisierung, Tracking und Zugriffskontrolle.
*/
export const Downloads: CollectionConfig = {
slug: 'downloads',
labels: {
singular: 'Download',
plural: 'Downloads',
},
admin: {
useAsTitle: 'title',
group: 'Content',
defaultColumns: ['title', 'category', 'fileType', 'downloadCount', 'isActive'],
description: 'Downloadbare Dateien und Dokumente',
},
access: {
read: tenantScopedPublicRead,
create: authenticatedOnly,
update: authenticatedOnly,
delete: authenticatedOnly,
},
fields: [
{
name: 'title',
type: 'text',
required: true,
label: 'Titel',
localized: true,
},
{
name: 'slug',
type: 'text',
required: true,
unique: true,
label: 'Slug',
},
{
name: 'description',
type: 'textarea',
label: 'Beschreibung',
localized: true,
},
{
name: 'file',
type: 'upload',
relationTo: 'media',
required: true,
label: 'Datei',
},
{
name: 'fileType',
type: 'select',
label: 'Dateityp',
options: [
{ label: 'PDF', value: 'pdf' },
{ label: 'Word', value: 'doc' },
{ label: 'Excel', value: 'xls' },
{ label: 'PowerPoint', value: 'ppt' },
{ label: 'Bild', value: 'image' },
{ label: 'Video', value: 'video' },
{ label: 'Audio', value: 'audio' },
{ label: 'ZIP/Archiv', value: 'archive' },
{ label: 'Sonstiges', value: 'other' },
],
admin: {
position: 'sidebar',
description: 'Wird automatisch erkannt, kann überschrieben werden',
},
},
{
name: 'fileSize',
type: 'text',
label: 'Dateigröße',
admin: {
position: 'sidebar',
description: 'z.B. "2.5 MB" - wird automatisch berechnet',
readOnly: true,
},
},
// Kategorisierung
{
name: 'category',
type: 'select',
label: 'Kategorie',
options: [
{ label: 'Broschüre', value: 'brochure' },
{ label: 'Flyer', value: 'flyer' },
{ label: 'Katalog', value: 'catalog' },
{ label: 'Preisliste', value: 'pricelist' },
{ label: 'Formular', value: 'form' },
{ label: 'Anleitung', value: 'manual' },
{ label: 'Datenblatt', value: 'datasheet' },
{ label: 'Zertifikat', value: 'certificate' },
{ label: 'Pressematerial', value: 'press' },
{ label: 'Whitepaper', value: 'whitepaper' },
{ label: 'Präsentation', value: 'presentation' },
{ label: 'Vertrag/AGB', value: 'legal' },
{ label: 'Sonstiges', value: 'other' },
],
},
{
name: 'tags',
type: 'array',
label: 'Tags',
fields: [
{
name: 'tag',
type: 'text',
required: true,
},
],
},
// Vorschau
{
name: 'thumbnail',
type: 'upload',
relationTo: 'media',
label: 'Vorschaubild',
admin: {
description: 'Optionales Thumbnail (sonst wird Standard-Icon verwendet)',
},
},
// Verknüpfungen
{
name: 'relatedServices',
type: 'relationship',
relationTo: 'services',
hasMany: true,
label: 'Zugehörige Services',
},
{
name: 'relatedProducts',
type: 'relationship',
relationTo: 'products',
hasMany: true,
label: 'Zugehörige Produkte',
},
// Zugriff
{
name: 'access',
type: 'group',
label: 'Zugriff',
fields: [
{
name: 'requireLogin',
type: 'checkbox',
defaultValue: false,
label: 'Login erforderlich',
},
{
name: 'requireForm',
type: 'checkbox',
defaultValue: false,
label: 'Formular vor Download',
admin: {
description: 'User müssen Kontaktdaten eingeben',
},
},
{
name: 'formFields',
type: 'select',
hasMany: true,
label: 'Formular-Felder',
options: [
{ label: 'Name', value: 'name' },
{ label: 'E-Mail', value: 'email' },
{ label: 'Firma', value: 'company' },
{ label: 'Telefon', value: 'phone' },
],
admin: {
condition: (_, siblingData) => siblingData?.requireForm,
},
},
],
},
// Tracking
{
name: 'downloadCount',
type: 'number',
defaultValue: 0,
label: 'Downloads',
admin: {
position: 'sidebar',
readOnly: true,
},
},
// Version
{
name: 'version',
type: 'text',
label: 'Version',
admin: {
description: 'z.B. "2024-01", "v2.0"',
},
},
{
name: 'lastUpdated',
type: 'date',
label: 'Letzte Aktualisierung',
},
// Status
{
name: 'isActive',
type: 'checkbox',
defaultValue: true,
label: 'Aktiv',
admin: {
position: 'sidebar',
},
},
{
name: 'isFeatured',
type: 'checkbox',
defaultValue: false,
label: 'Hervorgehoben',
admin: {
position: 'sidebar',
},
},
{
name: 'order',
type: 'number',
defaultValue: 0,
label: 'Sortierung',
admin: {
position: 'sidebar',
},
},
],
hooks: {
beforeChange: [
({ data }) => {
if (data && !data.slug && data.title) {
const titleStr = typeof data.title === 'string' ? data.title : ''
data.slug = titleStr
.toLowerCase()
.replace(/[äöüß]/g, (match: string) => {
const map: Record<string, string> = { ä: 'ae', ö: 'oe', ü: 'ue', ß: 'ss' }
return map[match] || match
})
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')
}
return data
},
],
},
}

757
src/collections/Events.ts Normal file
View file

@ -0,0 +1,757 @@
import type { CollectionConfig } from 'payload'
import { authenticatedOnly, tenantScopedPublicRead } from '../lib/tenantAccess'
/**
* Events Collection
*
* Veranstaltungen, Workshops, Seminare, Messen etc.
* Unterstützt einmalige und wiederkehrende Events, Online/Offline,
* Ticketing und Anmeldungen.
*/
export const Events: CollectionConfig = {
slug: 'events',
labels: {
singular: 'Veranstaltung',
plural: 'Veranstaltungen',
},
admin: {
useAsTitle: 'title',
group: 'Content',
defaultColumns: ['title', 'eventType', 'startDate', 'location', 'status'],
description: 'Veranstaltungen und Events',
},
access: {
read: tenantScopedPublicRead,
create: authenticatedOnly,
update: authenticatedOnly,
delete: authenticatedOnly,
},
fields: [
// Basis-Informationen
{
name: 'title',
type: 'text',
required: true,
label: 'Titel',
localized: true,
},
{
name: 'slug',
type: 'text',
required: true,
unique: true,
label: 'Slug',
admin: {
description: 'URL-Pfad für das Event',
},
},
{
name: 'subtitle',
type: 'text',
label: 'Untertitel',
localized: true,
},
{
name: 'description',
type: 'richText',
label: 'Beschreibung',
localized: true,
},
{
name: 'excerpt',
type: 'textarea',
label: 'Kurzbeschreibung',
localized: true,
admin: {
description: 'Für Übersichtsseiten und SEO',
},
},
// Event-Typ
{
name: 'eventType',
type: 'select',
required: true,
defaultValue: 'event',
label: 'Event-Typ',
options: [
{ label: 'Veranstaltung', value: 'event' },
{ label: 'Workshop', value: 'workshop' },
{ label: 'Seminar', value: 'seminar' },
{ label: 'Webinar', value: 'webinar' },
{ label: 'Konferenz', value: 'conference' },
{ label: 'Messe', value: 'tradeshow' },
{ label: 'Networking', value: 'networking' },
{ label: 'Kurs', value: 'course' },
{ label: 'Vortrag', value: 'talk' },
{ label: 'Sonstiges', value: 'other' },
],
},
{
name: 'category',
type: 'text',
label: 'Kategorie',
admin: {
description: 'Freies Textfeld für Kategorisierung',
},
},
// Format
{
name: 'format',
type: 'select',
required: true,
defaultValue: 'onsite',
label: 'Format',
options: [
{ label: 'Vor Ort', value: 'onsite' },
{ label: 'Online', value: 'online' },
{ label: 'Hybrid', value: 'hybrid' },
],
},
// Datum & Zeit
{
type: 'row',
fields: [
{
name: 'startDate',
type: 'date',
required: true,
label: 'Startdatum',
admin: {
date: {
pickerAppearance: 'dayAndTime',
},
width: '50%',
},
},
{
name: 'endDate',
type: 'date',
label: 'Enddatum',
admin: {
date: {
pickerAppearance: 'dayAndTime',
},
width: '50%',
description: 'Leer lassen für eintägige Events',
},
},
],
},
{
name: 'isAllDay',
type: 'checkbox',
defaultValue: false,
label: 'Ganztägig',
},
{
name: 'timezone',
type: 'text',
defaultValue: 'Europe/Berlin',
label: 'Zeitzone',
admin: {
description: 'z.B. Europe/Berlin, America/New_York',
},
},
// Wiederkehrende Events
{
name: 'isRecurring',
type: 'checkbox',
defaultValue: false,
label: 'Wiederkehrendes Event',
},
{
name: 'recurrence',
type: 'group',
label: 'Wiederholung',
admin: {
condition: (data, siblingData) => siblingData?.isRecurring,
},
fields: [
{
name: 'frequency',
type: 'select',
label: 'Häufigkeit',
options: [
{ label: 'Täglich', value: 'daily' },
{ label: 'Wöchentlich', value: 'weekly' },
{ label: 'Monatlich', value: 'monthly' },
{ label: 'Jährlich', value: 'yearly' },
],
},
{
name: 'interval',
type: 'number',
min: 1,
defaultValue: 1,
label: 'Intervall',
admin: {
description: 'Alle X Tage/Wochen/Monate/Jahre',
},
},
{
name: 'endRecurrence',
type: 'date',
label: 'Ende der Wiederholung',
},
{
name: 'description',
type: 'text',
label: 'Wiederholungs-Beschreibung',
localized: true,
admin: {
description: 'z.B. "Jeden ersten Montag im Monat"',
},
},
],
},
// Ort (für Vor-Ort Events)
{
name: 'location',
type: 'group',
label: 'Veranstaltungsort',
admin: {
condition: (data, siblingData) =>
siblingData?.format === 'onsite' || siblingData?.format === 'hybrid',
},
fields: [
{
name: 'locationRef',
type: 'relationship',
relationTo: 'locations' as 'users',
label: 'Standort auswählen',
admin: {
description: 'Aus bestehenden Standorten wählen',
},
},
{
name: 'customLocation',
type: 'checkbox',
defaultValue: false,
label: 'Eigene Adresse angeben',
},
{
name: 'venueName',
type: 'text',
label: 'Veranstaltungsort Name',
localized: true,
admin: {
condition: (data, siblingData) => siblingData?.customLocation,
},
},
{
name: 'street',
type: 'text',
label: 'Straße',
admin: {
condition: (data, siblingData) => siblingData?.customLocation,
},
},
{
name: 'zip',
type: 'text',
label: 'PLZ',
admin: {
condition: (data, siblingData) => siblingData?.customLocation,
},
},
{
name: 'city',
type: 'text',
label: 'Stadt',
admin: {
condition: (data, siblingData) => siblingData?.customLocation,
},
},
{
name: 'country',
type: 'text',
defaultValue: 'Deutschland',
label: 'Land',
admin: {
condition: (data, siblingData) => siblingData?.customLocation,
},
},
{
name: 'room',
type: 'text',
label: 'Raum/Saal',
localized: true,
},
{
name: 'directions',
type: 'textarea',
label: 'Anfahrt',
localized: true,
},
],
},
// Online-Details
{
name: 'online',
type: 'group',
label: 'Online-Details',
admin: {
condition: (data, siblingData) =>
siblingData?.format === 'online' || siblingData?.format === 'hybrid',
},
fields: [
{
name: 'platform',
type: 'select',
label: 'Plattform',
options: [
{ label: 'Zoom', value: 'zoom' },
{ label: 'Microsoft Teams', value: 'teams' },
{ label: 'Google Meet', value: 'google-meet' },
{ label: 'Webex', value: 'webex' },
{ label: 'GoToWebinar', value: 'gotowebinar' },
{ label: 'Livestream', value: 'livestream' },
{ label: 'Sonstiges', value: 'other' },
],
},
{
name: 'accessLink',
type: 'text',
label: 'Zugangslink',
admin: {
description: 'Wird nur angemeldeten Teilnehmern angezeigt',
},
},
{
name: 'accessInfo',
type: 'textarea',
label: 'Zugangsinformationen',
localized: true,
admin: {
description: 'z.B. Meeting-ID, Passwort',
},
},
],
},
// Medien
{
name: 'image',
type: 'upload',
relationTo: 'media',
label: 'Event-Bild',
},
{
name: 'gallery',
type: 'array',
label: 'Galerie',
fields: [
{
name: 'image',
type: 'upload',
relationTo: 'media',
required: true,
},
{
name: 'caption',
type: 'text',
localized: true,
},
],
},
// Anmeldung & Tickets
{
name: 'registration',
type: 'group',
label: 'Anmeldung',
fields: [
{
name: 'required',
type: 'checkbox',
defaultValue: false,
label: 'Anmeldung erforderlich',
},
{
name: 'method',
type: 'select',
label: 'Anmeldemethode',
defaultValue: 'form',
options: [
{ label: 'Internes Formular', value: 'form' },
{ label: 'E-Mail', value: 'email' },
{ label: 'Externer Link', value: 'external' },
{ label: 'Telefon', value: 'phone' },
],
admin: {
condition: (data, siblingData) => siblingData?.required,
},
},
{
name: 'formId',
type: 'relationship',
relationTo: 'forms',
label: 'Anmeldeformular',
admin: {
condition: (data, siblingData) => siblingData?.required && siblingData?.method === 'form',
},
},
{
name: 'email',
type: 'email',
label: 'Anmelde-E-Mail',
admin: {
condition: (data, siblingData) => siblingData?.required && siblingData?.method === 'email',
},
},
{
name: 'externalUrl',
type: 'text',
label: 'Externer Anmeldelink',
admin: {
condition: (data, siblingData) => siblingData?.required && siblingData?.method === 'external',
},
},
{
name: 'phone',
type: 'text',
label: 'Telefonnummer',
admin: {
condition: (data, siblingData) => siblingData?.required && siblingData?.method === 'phone',
},
},
{
name: 'deadline',
type: 'date',
label: 'Anmeldeschluss',
admin: {
condition: (data, siblingData) => siblingData?.required,
date: {
pickerAppearance: 'dayAndTime',
},
},
},
{
name: 'maxParticipants',
type: 'number',
min: 0,
label: 'Maximale Teilnehmer',
admin: {
condition: (data, siblingData) => siblingData?.required,
description: '0 = unbegrenzt',
},
},
{
name: 'currentParticipants',
type: 'number',
defaultValue: 0,
label: 'Aktuelle Anmeldungen',
admin: {
condition: (data, siblingData) => siblingData?.required,
readOnly: true,
},
},
{
name: 'waitlistEnabled',
type: 'checkbox',
defaultValue: false,
label: 'Warteliste aktivieren',
admin: {
condition: (data, siblingData) => siblingData?.required && siblingData?.maxParticipants > 0,
},
},
],
},
// Preise
{
name: 'pricing',
type: 'group',
label: 'Preise',
fields: [
{
name: 'isFree',
type: 'checkbox',
defaultValue: true,
label: 'Kostenlos',
},
{
name: 'prices',
type: 'array',
label: 'Preisoptionen',
admin: {
condition: (data, siblingData) => !siblingData?.isFree,
},
fields: [
{
name: 'name',
type: 'text',
required: true,
label: 'Bezeichnung',
localized: true,
admin: {
description: 'z.B. "Standard", "Frühbucher", "Ermäßigt"',
},
},
{
name: 'price',
type: 'number',
required: true,
min: 0,
label: 'Preis',
},
{
name: 'currency',
type: 'text',
defaultValue: 'EUR',
label: 'Währung',
},
{
name: 'description',
type: 'text',
label: 'Beschreibung',
localized: true,
},
{
name: 'validUntil',
type: 'date',
label: 'Gültig bis',
admin: {
description: 'Für Frühbucher-Preise',
},
},
],
},
{
name: 'priceNote',
type: 'text',
label: 'Preis-Hinweis',
localized: true,
admin: {
description: 'z.B. "zzgl. MwSt.", "inkl. Verpflegung"',
},
},
],
},
// Sprecher/Referenten
{
name: 'speakers',
type: 'array',
label: 'Referenten',
fields: [
{
name: 'teamMember',
type: 'relationship',
relationTo: 'team' as 'users',
label: 'Aus Team wählen',
},
{
name: 'isExternal',
type: 'checkbox',
defaultValue: false,
label: 'Externer Referent',
},
{
name: 'name',
type: 'text',
label: 'Name',
admin: {
condition: (data, siblingData) => siblingData?.isExternal,
},
},
{
name: 'title',
type: 'text',
label: 'Titel/Position',
localized: true,
admin: {
condition: (data, siblingData) => siblingData?.isExternal,
},
},
{
name: 'company',
type: 'text',
label: 'Unternehmen',
admin: {
condition: (data, siblingData) => siblingData?.isExternal,
},
},
{
name: 'photo',
type: 'upload',
relationTo: 'media',
label: 'Foto',
admin: {
condition: (data, siblingData) => siblingData?.isExternal,
},
},
{
name: 'bio',
type: 'textarea',
label: 'Kurz-Bio',
localized: true,
admin: {
condition: (data, siblingData) => siblingData?.isExternal,
},
},
{
name: 'role',
type: 'text',
label: 'Rolle beim Event',
localized: true,
admin: {
description: 'z.B. "Hauptredner", "Workshop-Leiter"',
},
},
],
},
// Agenda/Programm
{
name: 'agenda',
type: 'array',
label: 'Programm',
fields: [
{
name: 'time',
type: 'text',
label: 'Uhrzeit',
admin: {
description: 'z.B. "09:00 - 10:30"',
},
},
{
name: 'title',
type: 'text',
required: true,
label: 'Titel',
localized: true,
},
{
name: 'description',
type: 'textarea',
label: 'Beschreibung',
localized: true,
},
{
name: 'speaker',
type: 'text',
label: 'Referent',
},
{
name: 'type',
type: 'select',
label: 'Typ',
options: [
{ label: 'Vortrag', value: 'talk' },
{ label: 'Workshop', value: 'workshop' },
{ label: 'Pause', value: 'break' },
{ label: 'Networking', value: 'networking' },
{ label: 'Panel', value: 'panel' },
{ label: 'Q&A', value: 'qa' },
],
},
],
},
// Kontakt
{
name: 'contact',
type: 'group',
label: 'Ansprechpartner',
fields: [
{
name: 'teamMember',
type: 'relationship',
relationTo: 'team' as 'users',
label: 'Aus Team wählen',
},
{
name: 'name',
type: 'text',
label: 'Name',
},
{
name: 'email',
type: 'email',
label: 'E-Mail',
},
{
name: 'phone',
type: 'text',
label: 'Telefon',
},
],
},
// Verknüpfungen
{
name: 'relatedServices',
type: 'relationship',
relationTo: 'services' as 'users',
hasMany: true,
label: 'Verwandte Leistungen',
},
{
name: 'relatedEvents',
type: 'relationship',
relationTo: 'events' as 'users',
hasMany: true,
label: 'Verwandte Events',
},
// SEO
{
name: 'seo',
type: 'group',
label: 'SEO',
fields: [
{
name: 'metaTitle',
type: 'text',
label: 'Meta-Titel',
localized: true,
},
{
name: 'metaDescription',
type: 'textarea',
label: 'Meta-Beschreibung',
localized: true,
},
{
name: 'ogImage',
type: 'upload',
relationTo: 'media',
label: 'Social Media Bild',
},
],
},
// Status
{
name: 'status',
type: 'select',
required: true,
defaultValue: 'draft',
label: 'Status',
options: [
{ label: 'Entwurf', value: 'draft' },
{ label: 'Veröffentlicht', value: 'published' },
{ label: 'Ausgebucht', value: 'soldout' },
{ label: 'Abgesagt', value: 'cancelled' },
{ label: 'Verschoben', value: 'postponed' },
{ label: 'Beendet', value: 'past' },
],
admin: {
position: 'sidebar',
},
},
{
name: 'isFeatured',
type: 'checkbox',
defaultValue: false,
label: 'Hervorgehoben',
admin: {
position: 'sidebar',
},
},
{
name: 'publishedAt',
type: 'date',
label: 'Veröffentlichungsdatum',
admin: {
position: 'sidebar',
date: {
pickerAppearance: 'dayAndTime',
},
},
},
],
}

455
src/collections/Jobs.ts Normal file
View file

@ -0,0 +1,455 @@
import type { CollectionConfig } from 'payload'
import { authenticatedOnly, tenantScopedPublicRead } from '../lib/tenantAccess'
/**
* Jobs Collection
*
* Stellenanzeigen und Karriere-Seite.
* Unterstützt verschiedene Beschäftigungsarten, Standorte und Bewerbungsoptionen.
*/
export const Jobs: CollectionConfig = {
slug: 'jobs',
labels: {
singular: 'Stellenanzeige',
plural: 'Stellenanzeigen',
},
admin: {
useAsTitle: 'title',
group: 'Content',
defaultColumns: ['title', 'department', 'type', 'location', 'status', 'publishedAt'],
description: 'Stellenanzeigen und Karriere',
},
access: {
read: tenantScopedPublicRead,
create: authenticatedOnly,
update: authenticatedOnly,
delete: authenticatedOnly,
},
fields: [
// Basis-Informationen
{
name: 'title',
type: 'text',
required: true,
label: 'Stellentitel',
localized: true,
admin: {
description: 'z.B. "Pflegefachkraft (m/w/d)", "Senior Developer"',
},
},
{
name: 'slug',
type: 'text',
required: true,
unique: true,
label: 'Slug',
},
{
name: 'reference',
type: 'text',
label: 'Referenznummer',
admin: {
description: 'Interne Stellennummer',
},
},
// Kategorisierung
{
name: 'department',
type: 'text',
label: 'Abteilung',
localized: true,
admin: {
description: 'z.B. "Pflege", "IT", "Marketing"',
},
},
{
name: 'category',
type: 'select',
label: 'Kategorie',
options: [
{ label: 'Pflege & Betreuung', value: 'care' },
{ label: 'Medizin', value: 'medical' },
{ label: 'Verwaltung', value: 'admin' },
{ label: 'IT & Technik', value: 'it' },
{ label: 'Marketing', value: 'marketing' },
{ label: 'Vertrieb', value: 'sales' },
{ label: 'Finanzen', value: 'finance' },
{ label: 'Personal', value: 'hr' },
{ label: 'Produktion', value: 'production' },
{ label: 'Logistik', value: 'logistics' },
{ label: 'Sonstiges', value: 'other' },
],
},
{
name: 'type',
type: 'select',
required: true,
defaultValue: 'fulltime',
label: 'Beschäftigungsart',
options: [
{ label: 'Vollzeit', value: 'fulltime' },
{ label: 'Teilzeit', value: 'parttime' },
{ label: 'Minijob', value: 'minijob' },
{ label: 'Werkstudent', value: 'working_student' },
{ label: 'Praktikum', value: 'internship' },
{ label: 'Ausbildung', value: 'apprenticeship' },
{ label: 'Duales Studium', value: 'dual_study' },
{ label: 'Freelance', value: 'freelance' },
{ label: 'Befristet', value: 'temporary' },
],
admin: {
position: 'sidebar',
},
},
{
name: 'workModel',
type: 'select',
label: 'Arbeitsmodell',
options: [
{ label: 'Vor Ort', value: 'onsite' },
{ label: 'Remote', value: 'remote' },
{ label: 'Hybrid', value: 'hybrid' },
],
admin: {
position: 'sidebar',
},
},
{
name: 'experienceLevel',
type: 'select',
label: 'Erfahrungslevel',
options: [
{ label: 'Berufseinsteiger', value: 'entry' },
{ label: 'Mit Berufserfahrung', value: 'experienced' },
{ label: 'Senior', value: 'senior' },
{ label: 'Führungskraft', value: 'management' },
],
},
// Standort
{
name: 'location',
type: 'group',
label: 'Standort',
fields: [
{
name: 'locationRef',
type: 'relationship',
relationTo: 'locations',
label: 'Standort (aus Collection)',
},
{
name: 'city',
type: 'text',
label: 'Stadt',
admin: {
description: 'Falls kein Standort aus Collection',
},
},
{
name: 'region',
type: 'text',
label: 'Region/Bundesland',
},
{
name: 'country',
type: 'text',
defaultValue: 'Deutschland',
label: 'Land',
},
],
},
// Beschreibung
{
name: 'summary',
type: 'textarea',
label: 'Kurzbeschreibung',
localized: true,
maxLength: 300,
admin: {
description: 'Für Übersichten und SEO (max. 300 Zeichen)',
},
},
{
name: 'description',
type: 'richText',
label: 'Beschreibung',
localized: true,
},
// Strukturierte Abschnitte
{
name: 'responsibilities',
type: 'richText',
label: 'Aufgaben',
localized: true,
},
{
name: 'requirements',
type: 'richText',
label: 'Anforderungen',
localized: true,
},
{
name: 'qualifications',
type: 'richText',
label: 'Wünschenswerte Qualifikationen',
localized: true,
},
{
name: 'benefits',
type: 'richText',
label: 'Was wir bieten',
localized: true,
},
// Benefits als Liste
{
name: 'benefitsList',
type: 'array',
label: 'Benefits (Stichpunkte)',
admin: {
description: 'Für visuelle Darstellung mit Icons',
},
fields: [
{
name: 'benefit',
type: 'text',
required: true,
label: 'Benefit',
localized: true,
},
{
name: 'icon',
type: 'select',
label: 'Icon',
options: [
{ label: 'Gehalt', value: 'money' },
{ label: 'Urlaub', value: 'vacation' },
{ label: 'Homeoffice', value: 'home' },
{ label: 'Weiterbildung', value: 'education' },
{ label: 'Gesundheit', value: 'health' },
{ label: 'Team', value: 'team' },
{ label: 'Flexibel', value: 'flexible' },
{ label: 'Jobticket', value: 'transport' },
{ label: 'Essen', value: 'food' },
{ label: 'Kinderbetreuung', value: 'childcare' },
{ label: 'Fitness', value: 'fitness' },
{ label: 'Parkplatz', value: 'parking' },
],
},
],
},
// Gehalt
{
name: 'salary',
type: 'group',
label: 'Gehalt',
fields: [
{
name: 'show',
type: 'checkbox',
defaultValue: false,
label: 'Gehalt anzeigen',
},
{
name: 'type',
type: 'select',
label: 'Typ',
options: [
{ label: 'Jahresgehalt', value: 'yearly' },
{ label: 'Monatsgehalt', value: 'monthly' },
{ label: 'Stundenlohn', value: 'hourly' },
],
admin: {
condition: (_, siblingData) => siblingData?.show,
},
},
{
name: 'min',
type: 'number',
label: 'Von',
admin: {
condition: (_, siblingData) => siblingData?.show,
},
},
{
name: 'max',
type: 'number',
label: 'Bis',
admin: {
condition: (_, siblingData) => siblingData?.show,
},
},
{
name: 'currency',
type: 'text',
defaultValue: 'EUR',
label: 'Währung',
admin: {
condition: (_, siblingData) => siblingData?.show,
},
},
{
name: 'note',
type: 'text',
label: 'Hinweis',
localized: true,
admin: {
condition: (_, siblingData) => siblingData?.show,
description: 'z.B. "je nach Qualifikation"',
},
},
],
},
// Bewerbung
{
name: 'application',
type: 'group',
label: 'Bewerbung',
fields: [
{
name: 'method',
type: 'select',
defaultValue: 'email',
label: 'Bewerbungsweg',
options: [
{ label: 'Per E-Mail', value: 'email' },
{ label: 'Online-Formular', value: 'form' },
{ label: 'Externes Portal', value: 'external' },
{ label: 'Per Post', value: 'mail' },
],
},
{
name: 'email',
type: 'email',
label: 'Bewerbungs-E-Mail',
admin: {
condition: (_, siblingData) => siblingData?.method === 'email',
},
},
{
name: 'formId',
type: 'relationship',
relationTo: 'forms',
label: 'Bewerbungsformular',
admin: {
condition: (_, siblingData) => siblingData?.method === 'form',
},
},
{
name: 'externalUrl',
type: 'text',
label: 'Externes Portal (URL)',
admin: {
condition: (_, siblingData) => siblingData?.method === 'external',
},
},
{
name: 'contact',
type: 'relationship',
relationTo: 'team',
label: 'Ansprechpartner',
},
{
name: 'deadline',
type: 'date',
label: 'Bewerbungsfrist',
},
],
},
// Media
{
name: 'image',
type: 'upload',
relationTo: 'media',
label: 'Bild',
admin: {
description: 'Bild für die Stellenanzeige',
},
},
// Datum & Status
{
name: 'status',
type: 'select',
required: true,
defaultValue: 'draft',
label: 'Status',
options: [
{ label: 'Entwurf', value: 'draft' },
{ label: 'Veröffentlicht', value: 'published' },
{ label: 'Besetzt', value: 'filled' },
{ label: 'Archiviert', value: 'archived' },
],
admin: {
position: 'sidebar',
},
},
{
name: 'publishedAt',
type: 'date',
label: 'Veröffentlicht am',
admin: {
position: 'sidebar',
date: {
pickerAppearance: 'dayOnly',
},
},
},
{
name: 'isFeatured',
type: 'checkbox',
defaultValue: false,
label: 'Hervorgehoben',
admin: {
position: 'sidebar',
},
},
{
name: 'isUrgent',
type: 'checkbox',
defaultValue: false,
label: 'Dringend zu besetzen',
admin: {
position: 'sidebar',
},
},
// SEO
{
name: 'seo',
type: 'group',
label: 'SEO',
fields: [
{
name: 'metaTitle',
type: 'text',
label: 'Meta-Titel',
localized: true,
},
{
name: 'metaDescription',
type: 'textarea',
label: 'Meta-Beschreibung',
localized: true,
maxLength: 160,
},
],
},
],
hooks: {
beforeChange: [
({ data }) => {
if (data && !data.slug && data.title) {
const titleStr = typeof data.title === 'string' ? data.title : ''
data.slug = titleStr
.toLowerCase()
.replace(/[äöüß]/g, (match: string) => {
const map: Record<string, string> = { ä: 'ae', ö: 'oe', ü: 'ue', ß: 'ss' }
return map[match] || match
})
.replace(/\(m\/w\/d\)/gi, '')
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')
}
return data
},
],
},
}

View file

@ -0,0 +1,419 @@
import type { CollectionConfig } from 'payload'
import { authenticatedOnly, tenantScopedPublicRead } from '../lib/tenantAccess'
/**
* Locations Collection
*
* Firmenstandorte mit Adresse, Öffnungszeiten, Kontaktdaten und Kartenposition.
* Tenant-scoped für Multi-Tenant-Betrieb.
*/
export const Locations: CollectionConfig = {
slug: 'locations',
labels: {
singular: 'Standort',
plural: 'Standorte',
},
admin: {
useAsTitle: 'name',
group: 'Content',
defaultColumns: ['name', 'city', 'type', 'isMain', 'isActive'],
description: 'Firmenstandorte und Niederlassungen',
},
access: {
read: tenantScopedPublicRead,
create: authenticatedOnly,
update: authenticatedOnly,
delete: authenticatedOnly,
},
fields: [
// Basis-Informationen
{
name: 'name',
type: 'text',
required: true,
label: 'Name',
localized: true,
admin: {
description: 'z.B. "Hauptsitz München", "Filiale Hamburg"',
},
},
{
name: 'slug',
type: 'text',
required: true,
unique: true,
label: 'Slug',
admin: {
description: 'URL-freundlicher Identifier',
},
},
{
name: 'type',
type: 'select',
defaultValue: 'office',
label: 'Typ',
options: [
{ label: 'Hauptsitz', value: 'headquarters' },
{ label: 'Büro', value: 'office' },
{ label: 'Filiale', value: 'branch' },
{ label: 'Lager', value: 'warehouse' },
{ label: 'Showroom', value: 'showroom' },
{ label: 'Studio', value: 'studio' },
{ label: 'Praxis', value: 'practice' },
{ label: 'Werkstatt', value: 'workshop' },
],
},
{
name: 'description',
type: 'richText',
label: 'Beschreibung',
localized: true,
admin: {
description: 'Optionale Beschreibung des Standorts',
},
},
{
name: 'image',
type: 'upload',
relationTo: 'media',
label: 'Bild',
admin: {
description: 'Foto des Gebäudes oder Standorts',
},
},
// Adresse
{
name: 'address',
type: 'group',
label: 'Adresse',
fields: [
{
name: 'street',
type: 'text',
required: true,
label: 'Straße & Hausnummer',
},
{
name: 'additionalLine',
type: 'text',
label: 'Adresszusatz',
admin: {
description: 'z.B. "Gebäude B, 3. Stock"',
},
},
{
name: 'zip',
type: 'text',
required: true,
label: 'PLZ',
},
{
name: 'city',
type: 'text',
required: true,
label: 'Stadt',
},
{
name: 'state',
type: 'text',
label: 'Bundesland/Region',
},
{
name: 'country',
type: 'text',
defaultValue: 'Deutschland',
label: 'Land',
},
],
},
// Geo-Koordinaten
{
name: 'geo',
type: 'group',
label: 'Geo-Koordinaten',
admin: {
description: 'Für Kartenanzeige',
},
fields: [
{
name: 'lat',
type: 'number',
label: 'Breitengrad (Latitude)',
admin: {
step: 0.000001,
description: 'z.B. 48.137154',
},
},
{
name: 'lng',
type: 'number',
label: 'Längengrad (Longitude)',
admin: {
step: 0.000001,
description: 'z.B. 11.576124',
},
},
{
name: 'zoom',
type: 'number',
defaultValue: 15,
min: 1,
max: 20,
label: 'Zoom-Level',
},
],
},
// Kontaktdaten
{
name: 'contact',
type: 'group',
label: 'Kontaktdaten',
fields: [
{
name: 'phone',
type: 'text',
label: 'Telefon',
},
{
name: 'fax',
type: 'text',
label: 'Fax',
},
{
name: 'email',
type: 'email',
label: 'E-Mail',
},
{
name: 'website',
type: 'text',
label: 'Website',
admin: {
description: 'Falls abweichend von Hauptwebsite',
},
},
],
},
// Öffnungszeiten
{
name: 'hours',
type: 'group',
label: 'Öffnungszeiten',
fields: [
{
name: 'display',
type: 'textarea',
label: 'Freitext-Anzeige',
localized: true,
admin: {
description: 'z.B. "Mo-Fr: 9-18 Uhr, Sa: 10-14 Uhr"',
},
},
{
name: 'structured',
type: 'array',
label: 'Strukturierte Zeiten',
admin: {
description: 'Detaillierte Öffnungszeiten pro Tag',
initCollapsed: true,
},
fields: [
{
name: 'day',
type: 'select',
required: true,
label: 'Tag',
options: [
{ label: 'Montag', value: 'monday' },
{ label: 'Dienstag', value: 'tuesday' },
{ label: 'Mittwoch', value: 'wednesday' },
{ label: 'Donnerstag', value: 'thursday' },
{ label: 'Freitag', value: 'friday' },
{ label: 'Samstag', value: 'saturday' },
{ label: 'Sonntag', value: 'sunday' },
],
},
{
name: 'closed',
type: 'checkbox',
defaultValue: false,
label: 'Geschlossen',
},
{
name: 'open',
type: 'text',
label: 'Öffnet',
admin: {
condition: (_, siblingData) => !siblingData?.closed,
description: 'z.B. "09:00"',
},
},
{
name: 'close',
type: 'text',
label: 'Schließt',
admin: {
condition: (_, siblingData) => !siblingData?.closed,
description: 'z.B. "18:00"',
},
},
{
name: 'break',
type: 'group',
label: 'Mittagspause',
admin: {
condition: (_, siblingData) => !siblingData?.closed,
},
fields: [
{
name: 'hasBreak',
type: 'checkbox',
defaultValue: false,
label: 'Mittagspause',
},
{
name: 'start',
type: 'text',
label: 'Von',
admin: {
condition: (_, siblingData) => siblingData?.hasBreak,
},
},
{
name: 'end',
type: 'text',
label: 'Bis',
admin: {
condition: (_, siblingData) => siblingData?.hasBreak,
},
},
],
},
],
},
{
name: 'note',
type: 'text',
label: 'Hinweis',
localized: true,
admin: {
description: 'z.B. "Termine nach Vereinbarung"',
},
},
],
},
// Anfahrt
{
name: 'directions',
type: 'group',
label: 'Anfahrt',
fields: [
{
name: 'text',
type: 'richText',
label: 'Anfahrtsbeschreibung',
localized: true,
},
{
name: 'parking',
type: 'text',
label: 'Parkmöglichkeiten',
localized: true,
},
{
name: 'publicTransport',
type: 'text',
label: 'Öffentliche Verkehrsmittel',
localized: true,
admin: {
description: 'z.B. "U-Bahn U3 Haltestelle Münchner Freiheit"',
},
},
{
name: 'accessibility',
type: 'text',
label: 'Barrierefreiheit',
localized: true,
},
],
},
// Services an diesem Standort
{
name: 'services',
type: 'relationship',
relationTo: 'services',
hasMany: true,
label: 'Services an diesem Standort',
admin: {
description: 'Welche Dienstleistungen werden hier angeboten?',
},
},
// Team an diesem Standort
{
name: 'teamMembers',
type: 'relationship',
relationTo: 'team',
hasMany: true,
label: 'Team-Mitglieder',
admin: {
description: 'Mitarbeiter an diesem Standort',
},
},
// Status & Sortierung
{
name: 'isMain',
type: 'checkbox',
defaultValue: false,
label: 'Hauptstandort',
admin: {
position: 'sidebar',
description: 'Als primärer Standort markieren',
},
},
{
name: 'isActive',
type: 'checkbox',
defaultValue: true,
label: 'Aktiv',
admin: {
position: 'sidebar',
},
},
{
name: 'showInFooter',
type: 'checkbox',
defaultValue: false,
label: 'Im Footer anzeigen',
admin: {
position: 'sidebar',
},
},
{
name: 'order',
type: 'number',
defaultValue: 0,
label: 'Sortierung',
admin: {
position: 'sidebar',
},
},
],
hooks: {
beforeChange: [
({ data }) => {
// Auto-generate slug from name if not provided
if (data && !data.slug && data.name) {
data.slug = data.name
.toLowerCase()
.replace(/[äöüß]/g, (match: string) => {
const map: Record<string, string> = { ä: 'ae', ö: 'oe', ü: 'ue', ß: 'ss' }
return map[match] || match
})
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')
}
return data
},
],
},
}

View file

@ -28,6 +28,19 @@ import {
// Team Blocks // Team Blocks
TeamFilterBlock, TeamFilterBlock,
OrgChartBlock, OrgChartBlock,
// New Feature Blocks
LocationsBlock,
LogoGridBlock,
StatsBlock,
JobsBlock,
DownloadsBlock,
MapBlock,
// Events & Interactive Blocks
EventsBlock,
PricingBlock,
TabsBlock,
AccordionBlock,
ComparisonBlock,
} from '../blocks' } from '../blocks'
import { pagesAccess } from '../lib/access' import { pagesAccess } from '../lib/access'
@ -110,6 +123,19 @@ export const Pages: CollectionConfig = {
// Team Blocks // Team Blocks
TeamFilterBlock, TeamFilterBlock,
OrgChartBlock, OrgChartBlock,
// New Feature Blocks
LocationsBlock,
LogoGridBlock,
StatsBlock,
JobsBlock,
DownloadsBlock,
MapBlock,
// Events & Interactive Blocks
EventsBlock,
PricingBlock,
TabsBlock,
AccordionBlock,
ComparisonBlock,
], ],
}, },
{ {

274
src/collections/Partners.ts Normal file
View file

@ -0,0 +1,274 @@
import type { CollectionConfig } from 'payload'
import { authenticatedOnly, tenantScopedPublicRead } from '../lib/tenantAccess'
/**
* Partners Collection
*
* Partner, Kunden, Zertifizierungen und Referenzen mit Logos.
* Vielseitig einsetzbar für Logo-Walls, Referenzen, Zertifikate.
*/
export const Partners: CollectionConfig = {
slug: 'partners',
labels: {
singular: 'Partner',
plural: 'Partner',
},
admin: {
useAsTitle: 'name',
group: 'Content',
defaultColumns: ['name', 'type', 'isFeatured', 'isActive', 'order'],
description: 'Partner, Kunden, Referenzen und Zertifizierungen',
},
access: {
read: tenantScopedPublicRead,
create: authenticatedOnly,
update: authenticatedOnly,
delete: authenticatedOnly,
},
fields: [
{
name: 'name',
type: 'text',
required: true,
label: 'Name',
admin: {
description: 'Firmen-/Partnername',
},
},
{
name: 'slug',
type: 'text',
required: true,
unique: true,
label: 'Slug',
},
{
name: 'type',
type: 'select',
required: true,
defaultValue: 'partner',
label: 'Typ',
options: [
{ label: 'Partner', value: 'partner' },
{ label: 'Kunde', value: 'client' },
{ label: 'Referenz', value: 'reference' },
{ label: 'Sponsor', value: 'sponsor' },
{ label: 'Zertifizierung', value: 'certification' },
{ label: 'Mitgliedschaft', value: 'membership' },
{ label: 'Auszeichnung', value: 'award' },
{ label: 'Lieferant', value: 'supplier' },
{ label: 'Technologie', value: 'technology' },
],
admin: {
position: 'sidebar',
},
},
{
name: 'logo',
type: 'upload',
relationTo: 'media',
required: true,
label: 'Logo',
admin: {
description: 'Logo (empfohlen: SVG oder PNG mit transparentem Hintergrund)',
},
},
{
name: 'logoLight',
type: 'upload',
relationTo: 'media',
label: 'Logo (Hell)',
admin: {
description: 'Alternative Version für dunkle Hintergründe',
},
},
{
name: 'description',
type: 'textarea',
label: 'Beschreibung',
localized: true,
admin: {
description: 'Kurze Beschreibung der Partnerschaft',
},
},
{
name: 'website',
type: 'text',
label: 'Website',
},
// Für Referenzen/Case Studies
{
name: 'caseStudy',
type: 'group',
label: 'Case Study / Referenz',
admin: {
condition: (_, siblingData) =>
siblingData?.type === 'client' || siblingData?.type === 'reference',
},
fields: [
{
name: 'quote',
type: 'textarea',
label: 'Zitat',
localized: true,
},
{
name: 'quotePerson',
type: 'text',
label: 'Zitatgeber',
},
{
name: 'quoteRole',
type: 'text',
label: 'Position',
},
{
name: 'projectDescription',
type: 'richText',
label: 'Projektbeschreibung',
localized: true,
},
{
name: 'results',
type: 'array',
label: 'Ergebnisse/Kennzahlen',
fields: [
{
name: 'metric',
type: 'text',
required: true,
label: 'Kennzahl',
admin: {
description: 'z.B. "+50%", "10.000+"',
},
},
{
name: 'label',
type: 'text',
required: true,
label: 'Beschreibung',
localized: true,
admin: {
description: 'z.B. "Umsatzsteigerung", "Neue Kunden"',
},
},
],
},
],
},
// Für Zertifizierungen
{
name: 'certification',
type: 'group',
label: 'Zertifizierungs-Details',
admin: {
condition: (_, siblingData) =>
siblingData?.type === 'certification' ||
siblingData?.type === 'membership' ||
siblingData?.type === 'award',
},
fields: [
{
name: 'issuer',
type: 'text',
label: 'Aussteller',
},
{
name: 'validFrom',
type: 'date',
label: 'Gültig ab',
},
{
name: 'validUntil',
type: 'date',
label: 'Gültig bis',
},
{
name: 'certNumber',
type: 'text',
label: 'Zertifikatsnummer',
},
{
name: 'document',
type: 'upload',
relationTo: 'media',
label: 'Zertifikat (PDF)',
},
],
},
// Kategorisierung
{
name: 'category',
type: 'text',
label: 'Kategorie',
admin: {
description: 'z.B. "Technologie", "Finanzen", "Pflege"',
},
},
{
name: 'tags',
type: 'array',
label: 'Tags',
fields: [
{
name: 'tag',
type: 'text',
required: true,
},
],
},
// Status
{
name: 'isFeatured',
type: 'checkbox',
defaultValue: false,
label: 'Hervorgehoben',
admin: {
position: 'sidebar',
},
},
{
name: 'isActive',
type: 'checkbox',
defaultValue: true,
label: 'Aktiv',
admin: {
position: 'sidebar',
},
},
{
name: 'order',
type: 'number',
defaultValue: 0,
label: 'Sortierung',
admin: {
position: 'sidebar',
},
},
{
name: 'since',
type: 'date',
label: 'Partner seit',
admin: {
position: 'sidebar',
},
},
],
hooks: {
beforeChange: [
({ data }) => {
if (data && !data.slug && data.name) {
data.slug = data.name
.toLowerCase()
.replace(/[äöüß]/g, (match: string) => {
const map: Record<string, string> = { ä: 'ae', ö: 'oe', ü: 'ue', ß: 'ss' }
return map[match] || match
})
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')
}
return data
},
],
},
}

View file

@ -0,0 +1,604 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
// Create enums for locations
await db.execute(sql`
DO $$ BEGIN
CREATE TYPE "public"."enum_locations_hours_structured_day" AS ENUM('monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
`)
await db.execute(sql`
DO $$ BEGIN
CREATE TYPE "public"."enum_locations_type" AS ENUM('headquarters', 'office', 'branch', 'warehouse', 'showroom', 'studio', 'practice', 'workshop');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
`)
// Create enums for partners
await db.execute(sql`
DO $$ BEGIN
CREATE TYPE "public"."enum_partners_type" AS ENUM('partner', 'client', 'reference', 'sponsor', 'certification', 'membership', 'award', 'supplier', 'technology');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
`)
// Create enums for jobs
await db.execute(sql`
DO $$ BEGIN
CREATE TYPE "public"."enum_jobs_benefits_list_icon" AS ENUM('money', 'vacation', 'home', 'education', 'health', 'team', 'flexible', 'transport', 'food', 'childcare', 'fitness', 'parking');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
`)
await db.execute(sql`
DO $$ BEGIN
CREATE TYPE "public"."enum_jobs_category" AS ENUM('care', 'medical', 'admin', 'it', 'marketing', 'sales', 'finance', 'hr', 'production', 'logistics', 'other');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
`)
await db.execute(sql`
DO $$ BEGIN
CREATE TYPE "public"."enum_jobs_type" AS ENUM('fulltime', 'parttime', 'minijob', 'working_student', 'internship', 'apprenticeship', 'dual_study', 'freelance', 'temporary');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
`)
await db.execute(sql`
DO $$ BEGIN
CREATE TYPE "public"."enum_jobs_work_model" AS ENUM('onsite', 'remote', 'hybrid');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
`)
await db.execute(sql`
DO $$ BEGIN
CREATE TYPE "public"."enum_jobs_experience_level" AS ENUM('entry', 'experienced', 'senior', 'management');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
`)
await db.execute(sql`
DO $$ BEGIN
CREATE TYPE "public"."enum_jobs_salary_type" AS ENUM('yearly', 'monthly', 'hourly');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
`)
await db.execute(sql`
DO $$ BEGIN
CREATE TYPE "public"."enum_jobs_application_method" AS ENUM('email', 'form', 'external', 'mail');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
`)
await db.execute(sql`
DO $$ BEGIN
CREATE TYPE "public"."enum_jobs_status" AS ENUM('draft', 'published', 'filled', 'archived');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
`)
// Create enums for downloads
await db.execute(sql`
DO $$ BEGIN
CREATE TYPE "public"."enum_downloads_access_form_fields" AS ENUM('name', 'email', 'company', 'phone');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
`)
await db.execute(sql`
DO $$ BEGIN
CREATE TYPE "public"."enum_downloads_file_type" AS ENUM('pdf', 'doc', 'xls', 'ppt', 'image', 'video', 'audio', 'archive', 'other');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
`)
await db.execute(sql`
DO $$ BEGIN
CREATE TYPE "public"."enum_downloads_category" AS ENUM('brochure', 'flyer', 'catalog', 'pricelist', 'form', 'manual', 'datasheet', 'certificate', 'press', 'whitepaper', 'presentation', 'legal', 'other');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
`)
// Create locations table
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "locations" (
"id" serial PRIMARY KEY NOT NULL,
"tenant_id" integer REFERENCES "tenants"("id") ON DELETE SET NULL,
"slug" varchar NOT NULL,
"type" "enum_locations_type" DEFAULT 'office',
"image_id" integer REFERENCES "media"("id") ON DELETE SET NULL,
"address_street" varchar NOT NULL,
"address_additional_line" varchar,
"address_zip" varchar NOT NULL,
"address_city" varchar NOT NULL,
"address_state" varchar,
"address_country" varchar DEFAULT 'Deutschland',
"geo_lat" numeric,
"geo_lng" numeric,
"geo_zoom" numeric DEFAULT 15,
"contact_phone" varchar,
"contact_fax" varchar,
"contact_email" varchar,
"contact_website" varchar,
"is_main" boolean DEFAULT false,
"is_active" boolean DEFAULT true,
"show_in_footer" 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
);
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "locations_tenant_idx" ON "locations" USING btree ("tenant_id");
`)
await db.execute(sql`
CREATE UNIQUE INDEX IF NOT EXISTS "locations_slug_idx" ON "locations" USING btree ("slug");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "locations_image_idx" ON "locations" USING btree ("image_id");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "locations_updated_at_idx" ON "locations" USING btree ("updated_at");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "locations_created_at_idx" ON "locations" USING btree ("created_at");
`)
// Create locations_locales table
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "locations_locales" (
"name" varchar NOT NULL,
"description" jsonb,
"hours_display" varchar,
"hours_note" varchar,
"directions_text" jsonb,
"directions_parking" varchar,
"directions_public_transport" varchar,
"directions_accessibility" varchar,
"id" serial PRIMARY KEY NOT NULL,
"_locale" "_locales" NOT NULL,
"_parent_id" integer NOT NULL REFERENCES "locations"("id") ON DELETE CASCADE
);
`)
await db.execute(sql`
CREATE UNIQUE INDEX IF NOT EXISTS "locations_locales_locale_parent_id_unique" ON "locations_locales" USING btree ("_locale", "_parent_id");
`)
// Create locations_rels table
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "locations_rels" (
"id" serial PRIMARY KEY NOT NULL,
"order" integer,
"parent_id" integer NOT NULL REFERENCES "locations"("id") ON DELETE CASCADE,
"path" varchar NOT NULL,
"services_id" integer REFERENCES "services"("id") ON DELETE CASCADE,
"team_id" integer REFERENCES "team"("id") ON DELETE CASCADE
);
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "locations_rels_order_idx" ON "locations_rels" USING btree ("order");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "locations_rels_parent_idx" ON "locations_rels" USING btree ("parent_id");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "locations_rels_path_idx" ON "locations_rels" USING btree ("path");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "locations_rels_services_id_idx" ON "locations_rels" USING btree ("services_id");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "locations_rels_team_id_idx" ON "locations_rels" USING btree ("team_id");
`)
// Create partners table
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "partners" (
"id" serial PRIMARY KEY NOT NULL,
"tenant_id" integer REFERENCES "tenants"("id") ON DELETE SET NULL,
"name" varchar NOT NULL,
"slug" varchar NOT NULL,
"type" "enum_partners_type" NOT NULL DEFAULT 'partner',
"logo_id" integer NOT NULL REFERENCES "media"("id") ON DELETE SET NULL,
"logo_light_id" integer REFERENCES "media"("id") ON DELETE SET NULL,
"website" varchar,
"case_study_quote_person" varchar,
"case_study_quote_role" varchar,
"certification_issuer" varchar,
"certification_valid_from" timestamp(3) with time zone,
"certification_valid_until" timestamp(3) with time zone,
"certification_cert_number" varchar,
"certification_document_id" integer REFERENCES "media"("id") ON DELETE SET NULL,
"category" varchar,
"is_featured" boolean DEFAULT false,
"is_active" boolean DEFAULT true,
"order" numeric DEFAULT 0,
"since" timestamp(3) with time zone,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "partners_tenant_idx" ON "partners" USING btree ("tenant_id");
`)
await db.execute(sql`
CREATE UNIQUE INDEX IF NOT EXISTS "partners_slug_idx" ON "partners" USING btree ("slug");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "partners_logo_idx" ON "partners" USING btree ("logo_id");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "partners_logo_light_idx" ON "partners" USING btree ("logo_light_id");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "partners_certification_certification_document_idx" ON "partners" USING btree ("certification_document_id");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "partners_updated_at_idx" ON "partners" USING btree ("updated_at");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "partners_created_at_idx" ON "partners" USING btree ("created_at");
`)
// Create partners_locales table
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "partners_locales" (
"description" varchar,
"case_study_quote" varchar,
"case_study_project_description" jsonb,
"id" serial PRIMARY KEY NOT NULL,
"_locale" "_locales" NOT NULL,
"_parent_id" integer NOT NULL REFERENCES "partners"("id") ON DELETE CASCADE
);
`)
await db.execute(sql`
CREATE UNIQUE INDEX IF NOT EXISTS "partners_locales_locale_parent_id_unique" ON "partners_locales" USING btree ("_locale", "_parent_id");
`)
// Create partners_case_study_results table
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "partners_case_study_results" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL REFERENCES "partners"("id") ON DELETE CASCADE,
"id" varchar PRIMARY KEY NOT NULL,
"metric" varchar
);
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "partners_case_study_results_order_idx" ON "partners_case_study_results" USING btree ("_order");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "partners_case_study_results_parent_id_idx" ON "partners_case_study_results" USING btree ("_parent_id");
`)
// Create partners_case_study_results_locales table
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "partners_case_study_results_locales" (
"label" varchar,
"id" serial PRIMARY KEY NOT NULL,
"_locale" "_locales" NOT NULL,
"_parent_id" varchar NOT NULL REFERENCES "partners_case_study_results"("id") ON DELETE CASCADE
);
`)
await db.execute(sql`
CREATE UNIQUE INDEX IF NOT EXISTS "partners_case_study_results_locales_locale_parent_id_unique" ON "partners_case_study_results_locales" USING btree ("_locale", "_parent_id");
`)
// Create partners_tags table
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "partners_tags" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL REFERENCES "partners"("id") ON DELETE CASCADE,
"id" varchar PRIMARY KEY NOT NULL,
"tag" varchar NOT NULL
);
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "partners_tags_order_idx" ON "partners_tags" USING btree ("_order");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "partners_tags_parent_id_idx" ON "partners_tags" USING btree ("_parent_id");
`)
// Create jobs table
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "jobs" (
"id" serial PRIMARY KEY NOT NULL,
"tenant_id" integer REFERENCES "tenants"("id") ON DELETE SET NULL,
"slug" varchar NOT NULL,
"reference" varchar,
"category" "enum_jobs_category",
"type" "enum_jobs_type" NOT NULL DEFAULT 'fulltime',
"work_model" "enum_jobs_work_model",
"experience_level" "enum_jobs_experience_level",
"location_location_ref_id" integer REFERENCES "locations"("id") ON DELETE SET NULL,
"location_city" varchar,
"location_region" varchar,
"location_country" varchar DEFAULT 'Deutschland',
"salary_show" boolean DEFAULT false,
"salary_type" "enum_jobs_salary_type",
"salary_min" numeric,
"salary_max" numeric,
"salary_currency" varchar DEFAULT 'EUR',
"application_method" "enum_jobs_application_method" DEFAULT 'email',
"application_email" varchar,
"application_form_id_id" integer REFERENCES "forms"("id") ON DELETE SET NULL,
"application_external_url" varchar,
"application_contact_id" integer REFERENCES "team"("id") ON DELETE SET NULL,
"application_deadline" timestamp(3) with time zone,
"image_id" integer REFERENCES "media"("id") ON DELETE SET NULL,
"status" "enum_jobs_status" NOT NULL DEFAULT 'draft',
"published_at" timestamp(3) with time zone,
"is_featured" boolean DEFAULT false,
"is_urgent" boolean DEFAULT false,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "jobs_tenant_idx" ON "jobs" USING btree ("tenant_id");
`)
await db.execute(sql`
CREATE UNIQUE INDEX IF NOT EXISTS "jobs_slug_idx" ON "jobs" USING btree ("slug");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "jobs_location_location_location_ref_idx" ON "jobs" USING btree ("location_location_ref_id");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "jobs_application_application_form_id_idx" ON "jobs" USING btree ("application_form_id_id");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "jobs_application_application_contact_idx" ON "jobs" USING btree ("application_contact_id");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "jobs_image_idx" ON "jobs" USING btree ("image_id");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "jobs_updated_at_idx" ON "jobs" USING btree ("updated_at");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "jobs_created_at_idx" ON "jobs" USING btree ("created_at");
`)
// Create jobs_locales table
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "jobs_locales" (
"title" varchar NOT NULL,
"department" varchar,
"summary" varchar,
"description" jsonb,
"responsibilities" jsonb,
"requirements" jsonb,
"qualifications" jsonb,
"benefits" jsonb,
"salary_note" varchar,
"seo_meta_title" varchar,
"seo_meta_description" varchar,
"id" serial PRIMARY KEY NOT NULL,
"_locale" "_locales" NOT NULL,
"_parent_id" integer NOT NULL REFERENCES "jobs"("id") ON DELETE CASCADE
);
`)
await db.execute(sql`
CREATE UNIQUE INDEX IF NOT EXISTS "jobs_locales_locale_parent_id_unique" ON "jobs_locales" USING btree ("_locale", "_parent_id");
`)
// Create jobs_benefits_list table
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "jobs_benefits_list" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL REFERENCES "jobs"("id") ON DELETE CASCADE,
"id" varchar PRIMARY KEY NOT NULL,
"icon" "enum_jobs_benefits_list_icon"
);
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "jobs_benefits_list_order_idx" ON "jobs_benefits_list" USING btree ("_order");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "jobs_benefits_list_parent_id_idx" ON "jobs_benefits_list" USING btree ("_parent_id");
`)
// Create jobs_benefits_list_locales table
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "jobs_benefits_list_locales" (
"benefit" varchar NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"_locale" "_locales" NOT NULL,
"_parent_id" varchar NOT NULL REFERENCES "jobs_benefits_list"("id") ON DELETE CASCADE
);
`)
await db.execute(sql`
CREATE UNIQUE INDEX IF NOT EXISTS "jobs_benefits_list_locales_locale_parent_id_unique" ON "jobs_benefits_list_locales" USING btree ("_locale", "_parent_id");
`)
// Create downloads table
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "downloads" (
"id" serial PRIMARY KEY NOT NULL,
"tenant_id" integer REFERENCES "tenants"("id") ON DELETE SET NULL,
"slug" varchar NOT NULL,
"file_id" integer NOT NULL REFERENCES "media"("id") ON DELETE SET NULL,
"file_type" "enum_downloads_file_type",
"file_size" varchar,
"category" "enum_downloads_category",
"thumbnail_id" integer REFERENCES "media"("id") ON DELETE SET NULL,
"access_require_login" boolean DEFAULT false,
"access_require_form" boolean DEFAULT false,
"download_count" numeric DEFAULT 0,
"version" varchar,
"last_updated" timestamp(3) with time zone,
"is_active" boolean DEFAULT true,
"is_featured" 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
);
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "downloads_tenant_idx" ON "downloads" USING btree ("tenant_id");
`)
await db.execute(sql`
CREATE UNIQUE INDEX IF NOT EXISTS "downloads_slug_idx" ON "downloads" USING btree ("slug");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "downloads_file_idx" ON "downloads" USING btree ("file_id");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "downloads_thumbnail_idx" ON "downloads" USING btree ("thumbnail_id");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "downloads_updated_at_idx" ON "downloads" USING btree ("updated_at");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "downloads_created_at_idx" ON "downloads" USING btree ("created_at");
`)
// Create downloads_locales table
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "downloads_locales" (
"title" varchar NOT NULL,
"description" varchar,
"id" serial PRIMARY KEY NOT NULL,
"_locale" "_locales" NOT NULL,
"_parent_id" integer NOT NULL REFERENCES "downloads"("id") ON DELETE CASCADE
);
`)
await db.execute(sql`
CREATE UNIQUE INDEX IF NOT EXISTS "downloads_locales_locale_parent_id_unique" ON "downloads_locales" USING btree ("_locale", "_parent_id");
`)
// Create downloads_tags table
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "downloads_tags" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL REFERENCES "downloads"("id") ON DELETE CASCADE,
"id" varchar PRIMARY KEY NOT NULL,
"tag" varchar NOT NULL
);
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "downloads_tags_order_idx" ON "downloads_tags" USING btree ("_order");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "downloads_tags_parent_id_idx" ON "downloads_tags" USING btree ("_parent_id");
`)
// Create downloads_access_form_fields table
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "downloads_access_form_fields" (
"order" integer NOT NULL,
"parent_id" integer NOT NULL REFERENCES "downloads"("id") ON DELETE CASCADE,
"value" "enum_downloads_access_form_fields",
"id" serial PRIMARY KEY NOT NULL
);
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "downloads_access_form_fields_order_idx" ON "downloads_access_form_fields" USING btree ("order");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "downloads_access_form_fields_parent_idx" ON "downloads_access_form_fields" USING btree ("parent_id");
`)
// Create downloads_rels table
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "downloads_rels" (
"id" serial PRIMARY KEY NOT NULL,
"order" integer,
"parent_id" integer NOT NULL REFERENCES "downloads"("id") ON DELETE CASCADE,
"path" varchar NOT NULL,
"services_id" integer REFERENCES "services"("id") ON DELETE CASCADE,
"products_id" integer REFERENCES "products"("id") ON DELETE CASCADE
);
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "downloads_rels_order_idx" ON "downloads_rels" USING btree ("order");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "downloads_rels_parent_idx" ON "downloads_rels" USING btree ("parent_id");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "downloads_rels_path_idx" ON "downloads_rels" USING btree ("path");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "downloads_rels_services_id_idx" ON "downloads_rels" USING btree ("services_id");
`)
await db.execute(sql`
CREATE INDEX IF NOT EXISTS "downloads_rels_products_id_idx" ON "downloads_rels" USING btree ("products_id");
`)
}
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
// Drop tables in reverse order (respecting foreign key constraints)
await db.execute(sql`DROP TABLE IF EXISTS "downloads_rels" CASCADE;`)
await db.execute(sql`DROP TABLE IF EXISTS "downloads_access_form_fields" CASCADE;`)
await db.execute(sql`DROP TABLE IF EXISTS "downloads_tags" CASCADE;`)
await db.execute(sql`DROP TABLE IF EXISTS "downloads_locales" CASCADE;`)
await db.execute(sql`DROP TABLE IF EXISTS "downloads" CASCADE;`)
await db.execute(sql`DROP TABLE IF EXISTS "jobs_benefits_list_locales" CASCADE;`)
await db.execute(sql`DROP TABLE IF EXISTS "jobs_benefits_list" CASCADE;`)
await db.execute(sql`DROP TABLE IF EXISTS "jobs_locales" CASCADE;`)
await db.execute(sql`DROP TABLE IF EXISTS "jobs" CASCADE;`)
await db.execute(sql`DROP TABLE IF EXISTS "partners_tags" CASCADE;`)
await db.execute(sql`DROP TABLE IF EXISTS "partners_case_study_results_locales" CASCADE;`)
await db.execute(sql`DROP TABLE IF EXISTS "partners_case_study_results" CASCADE;`)
await db.execute(sql`DROP TABLE IF EXISTS "partners_locales" CASCADE;`)
await db.execute(sql`DROP TABLE IF EXISTS "partners" CASCADE;`)
await db.execute(sql`DROP TABLE IF EXISTS "locations_rels" CASCADE;`)
await db.execute(sql`DROP TABLE IF EXISTS "locations_locales" CASCADE;`)
await db.execute(sql`DROP TABLE IF EXISTS "locations" CASCADE;`)
// Drop enums
await db.execute(sql`DROP TYPE IF EXISTS "enum_downloads_category" CASCADE;`)
await db.execute(sql`DROP TYPE IF EXISTS "enum_downloads_file_type" CASCADE;`)
await db.execute(sql`DROP TYPE IF EXISTS "enum_downloads_access_form_fields" CASCADE;`)
await db.execute(sql`DROP TYPE IF EXISTS "enum_jobs_status" CASCADE;`)
await db.execute(sql`DROP TYPE IF EXISTS "enum_jobs_application_method" CASCADE;`)
await db.execute(sql`DROP TYPE IF EXISTS "enum_jobs_salary_type" CASCADE;`)
await db.execute(sql`DROP TYPE IF EXISTS "enum_jobs_experience_level" CASCADE;`)
await db.execute(sql`DROP TYPE IF EXISTS "enum_jobs_work_model" CASCADE;`)
await db.execute(sql`DROP TYPE IF EXISTS "enum_jobs_type" CASCADE;`)
await db.execute(sql`DROP TYPE IF EXISTS "enum_jobs_category" CASCADE;`)
await db.execute(sql`DROP TYPE IF EXISTS "enum_jobs_benefits_list_icon" CASCADE;`)
await db.execute(sql`DROP TYPE IF EXISTS "enum_partners_type" CASCADE;`)
await db.execute(sql`DROP TYPE IF EXISTS "enum_locations_type" CASCADE;`)
await db.execute(sql`DROP TYPE IF EXISTS "enum_locations_hours_structured_day" CASCADE;`)
}

View file

@ -12,6 +12,11 @@ import * as migration_20251212_211506_add_products_collections from './20251212_
import * as migration_20251213_100753_add_timelines_collection from './20251213_100753_add_timelines_collection'; import * as migration_20251213_100753_add_timelines_collection from './20251213_100753_add_timelines_collection';
import * as migration_20251213_104523_add_workflows_and_timeline_process_fields from './20251213_104523_add_workflows_and_timeline_process_fields'; import * as migration_20251213_104523_add_workflows_and_timeline_process_fields from './20251213_104523_add_workflows_and_timeline_process_fields';
import * as migration_20251213_145438_hero_slider_block from './20251213_145438_hero_slider_block'; import * as migration_20251213_145438_hero_slider_block from './20251213_145438_hero_slider_block';
import * as migration_20251213_160000_testimonials_slider_v2 from './20251213_160000_testimonials_slider_v2';
import * as migration_20251213_210000_image_slider_block from './20251213_210000_image_slider_block';
import * as migration_20251213_220000_blogging_collections from './20251213_220000_blogging_collections';
import * as migration_20251213_230000_team_extensions from './20251213_230000_team_extensions';
import * as migration_20251214_000000_add_priority_collections from './20251214_000000_add_priority_collections';
export const migrations = [ export const migrations = [
{ {
@ -82,6 +87,31 @@ export const migrations = [
{ {
up: migration_20251213_145438_hero_slider_block.up, up: migration_20251213_145438_hero_slider_block.up,
down: migration_20251213_145438_hero_slider_block.down, down: migration_20251213_145438_hero_slider_block.down,
name: '20251213_145438_hero_slider_block' name: '20251213_145438_hero_slider_block',
},
{
up: migration_20251213_160000_testimonials_slider_v2.up,
down: migration_20251213_160000_testimonials_slider_v2.down,
name: '20251213_160000_testimonials_slider_v2',
},
{
up: migration_20251213_210000_image_slider_block.up,
down: migration_20251213_210000_image_slider_block.down,
name: '20251213_210000_image_slider_block',
},
{
up: migration_20251213_220000_blogging_collections.up,
down: migration_20251213_220000_blogging_collections.down,
name: '20251213_220000_blogging_collections',
},
{
up: migration_20251213_230000_team_extensions.up,
down: migration_20251213_230000_team_extensions.down,
name: '20251213_230000_team_extensions',
},
{
up: migration_20251214_000000_add_priority_collections.up,
down: migration_20251214_000000_add_priority_collections.down,
name: '20251214_000000_add_priority_collections',
}, },
]; ];

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -48,6 +48,13 @@ import { Workflows } from './collections/Workflows'
import { Tags } from './collections/Tags' import { Tags } from './collections/Tags'
import { Authors } from './collections/Authors' import { Authors } from './collections/Authors'
// New Feature Collections
import { Locations } from './collections/Locations'
import { Partners } from './collections/Partners'
import { Jobs } from './collections/Jobs'
import { Downloads } from './collections/Downloads'
import { Events } from './collections/Events'
// Consent Management Collections // Consent Management Collections
import { CookieConfigurations } from './collections/CookieConfigurations' import { CookieConfigurations } from './collections/CookieConfigurations'
import { CookieInventory } from './collections/CookieInventory' import { CookieInventory } from './collections/CookieInventory'
@ -168,6 +175,12 @@ export default buildConfig({
// Blogging // Blogging
Tags, Tags,
Authors, Authors,
// New Feature Collections
Locations,
Partners,
Jobs,
Downloads,
Events,
// Consent Management // Consent Management
CookieConfigurations, CookieConfigurations,
CookieInventory, CookieInventory,
@ -187,9 +200,8 @@ export default buildConfig({
pool: { pool: {
connectionString: env.DATABASE_URI, connectionString: env.DATABASE_URI,
}, },
// Deaktiviere automatisches Schema-Push // Temporär aktiviert für Events Collection
// Änderungen sollten nur über Migrationen erfolgen push: true,
push: false,
}), }),
// Sharp für Bildoptimierung // Sharp für Bildoptimierung
sharp, sharp,
@ -222,6 +234,12 @@ export default buildConfig({
// Blogging Collections // Blogging Collections
tags: {}, tags: {},
authors: {}, authors: {},
// New Feature Collections
locations: {},
partners: {},
jobs: {},
downloads: {},
events: {},
// Consent Management Collections - customTenantField: true weil sie bereits ein tenant-Feld haben // Consent Management Collections - customTenantField: true weil sie bereits ein tenant-Feld haben
'cookie-configurations': { customTenantField: true }, 'cookie-configurations': { customTenantField: true },
'cookie-inventory': { customTenantField: true }, 'cookie-inventory': { customTenantField: true },